无非是各个框架或者类库的使用方法不一样,但是漏洞基础原理是相同的,这需要针对性对每个不同的语言的应用使用的错误方式进行总结。

目录

1 文件操作类

目录遍历

只能够看到目录下有哪些文件。

任意文件上传

任意文件下载

文件解压

2 注入类

SQL 注入

现在注入很少了,是因为大家都用 ORM 框架,而框架本身默认让你使用预编译,所以注入少了。

注入本质就是注入的 SQL 语句,只要你能够逃脱原有 SQL 语句控制,去单独执行注入新的 SQL 语句就算注入成功。

代码审计不太关注这个注入到底获取速度多快,但实战中需要密切关注。获取数据的速度排序,快到慢:

  • 报错和联合
  • OOB
  • 布尔
  • 延时

编写 SQL 需要你了解 SQL 语句标准写法,各个数据库的对 SQL 标准的补充写法。这可以帮助你绕过 WAF 和一些代码层面的过滤。

各大语言常见的 ORM 框架:

  • PHP 语言没有什么 ORM 框架,都是通过 Web 框架做的包装。
  • C Sharp 语言 Web 常用的框架有 ASP.NET Core,常用 ORM 框架是 NHibernate
  • Python 语言 Web 常用的框架有 Flask、Django,常用 ORM 框架是 SQLAlchemy
  • Java 语言 Web 常用的框架有 Spring、Struts2 常用 ORM 框架是 MyBatis 和 Hibernate

宽字节注入:

  • 查看数据库连接编码是不是 GBK
  • addslashed 对输入的字符做了编码
  • 挖掘这种洞可以看看配置文件 mysqli_set_charset 连接数据库时指定的字符集

HQL 语句注入:需要先绕过 HQL

预编译常见注入点(没有使用数据库做字符串拼接):

  • 模糊查询 like
  • 排序 order by

远程命令执行

取决于你能执行什么命令,如果任何命令都能执行,那么结合 Linux 下 Shell 特性很容器拿到 Shell。但是有些方法不支持 Shell 运行命令,直接执行系统命令,比如 Python 的 subprocess.run 它的参数 shell=False,这样就不会创建 Shell 进程再由 Shell 进程执行系统命令,没法逃逸 Shell。

Linux 具体这些程序有没调用 Shell 再执行命令可以通过 strace 看看 syscall,因为创建 Shell 就会调 fork,执行命令最终会调 execve。

strace -tt -f -e trace=process {要执行的程序|-p 要patch应用的pid}

PHP

exec — 执⾏⼀个外部程序
passthru — 执⾏外部程序并且显示原始输出
proc_open — 执⾏⼀个命令,并且打开⽤来输⼊/输出的⽂件指针。
shell_exec — 通过 shell 执⾏命令并将完整的输出以字符串的⽅式返回
system — 执⾏外部程序,并且显示输出

Python

os.system() #执⾏系统指令
os.popen()  #popen()⽅法⽤于从⼀个命令打开⼀个管道
subprocess.call #执⾏由参数提供的命令

Java

Runtime.getRuntime().exec()
ProcessBuilder()

如果只能应用固定命令,把输入作为参数附加到命令后面,那就看见 GTFOBinsLOLBAS 有没这些程序利用方式。

远程代码执行

输入的数据能够直接走到系统中指定代码的方法里。

PHP

eval() //把字符串作为PHP代码执⾏
assert() //检查⼀个断⾔是否为 FALSE,可⽤来执⾏代码
preg_replace() //执⾏⼀个正则表达式的搜索和替换
call_user_func() //把第⼀个参数作为回调函数调⽤
call_user_func_array() //调⽤回调函数,并把⼀个数组参数作为回调函数的参数
array_map() //为数组的每个元素应⽤回调函数
$a="ev"."al";$b=$_GET['a'];$a($b) //通过变量的方式拼接成动态函数,WebShell 比较常见

Python

exec(string) # Python代码的动态执⾏
eval(string) # 返回表达式或代码对象的值
execfile(string) # 从⼀个⽂件中读取和执⾏Python脚本

Java 中没有代码执行的方法。

反序列化

https://github.com/LxxxSec/CTF-Java-Gadget?tab=readme-ov-file

实战中不会去挖利用链,日常项目中

XSS

这里不要局限于后端,前端框架 XSS 也要关注,比如 Vue、React。

表达式注入

EL 表达式注入

JSP 上会用到。

SPEL 表达式注入

Spring 框架会用到

ONGL 表达式注入

Struts 框架上会用到

SSTI

XXE

3 访问控制

垂直越权

水平越权

认证缺失

OAuth2

SpringSecurity

Shiro

JWT

4 业务逻辑

验证码

5 其他

CORS 跨域

点击劫持

HTTP参数污染

CSRF

SSRF

整数溢出

异常未处理

不安全的随机数

硬编码密码

变量覆盖

变量覆盖(CWE-473: PHP External Variable Modification)是指通过输入能够控制某个变量的值或者创建一个新变量,仅仅能够覆盖值并没有用,只能算风险点,在实际利用中往往需要跟踪这个变量流向,到底最终能产生什么漏洞,比如这个变量最终传到数据库查询,并没有过滤,那么就是变量覆盖导致 SQL 注入。变量覆盖并不只是 PHP 专有,其他语言也有但不常见。

PS:随着 PHP 框架化使用变多,变量覆盖越来越少。

在语法上要重点掌握,以下四种解析操作。

  • $$
  • extract()
  • parse_str()
  • mb_parse_str()

其余的内容都已经过时,不需要关注。

$$

这种语法叫可变变量(或者引⽤变量、动态变量,都指的同一个内容),创建变量时把某个变量的值用作当前新变量的名称。

doubleDollar1.php

<?php  
$a = 'hello';  
$$a = 'world';  
  
echo $a . "\n";  
echo "{$$a}\n";  
echo $$a . "\n";  
echo $hello;

根据输出可以看到 $hello 变量明明没有定义却能输出值,这里创建的新变量 $hello 变量标识符 hello 是 $a 变量的值,通过 $$ 创建变量把 hello 作为新标识符,所以 $hello 变量创建成功了。

hello
world
world
world

下面看一个覆盖案例 doubleDollar2.php。

<?php
$test = 1;

foreach(array('_COOKIE','_POST','_GET') as $_request) {
    foreach($$_request as $_key=>$_value) {
        $$_key=addslashes($_value);
    }
    // 可以省去大括号缩写
    // foreach($$_request as $_key => $_value) ${$_key} = addslashes($_value);
}

echo $test;

第一层 foreach 中 array('_COOKIE','_POST','_GET') as $_request 是创建一个数组,最后创建变量 $_request 指向数组。

第二层 foreach 中 $$_request as $_key=>$_value 是把数组的每一个值引用成变量,以数组第一个字符串 _COOKIE 为例,使用 $$ 把字符串 _COOKIE 变成 $_COOKIE,而 $_COOKIE 本来就存在所以只是引用,获取到 COOKIE 数组后里面的键赋值给 $_key,值赋值给 $_value。最后通过把键名字符串使用 $$ 创建成新变量名进行赋值。

简单来说就是把 GET、POST、Cookie 中的参数,键作为变量名,值则是参数值。

这里就很明显存在变量覆盖,只要传递在 GET、POST 上传递 test 参数或者在 Cookie 请求投添加 test 对键值对就可以将上面的 $test 变量重新赋值。

1.Cookie 覆盖

C:\Users\gbb>curl -H "Cookie: Test=abc" http://172.18.91.76/ExternalVariableModification/doubleDollar2.php -v
*   Trying 172.18.91.76:80...
* Connected to 172.18.91.76 (172.18.91.76) port 80
> GET /ExternalVariableModification/doubleDollar2.php HTTP/1.1
> Host: 172.18.91.76
> User-Agent: curl/8.9.1
> Accept: */*
> Cookie: Test=abc
>
* Request completely sent off
< HTTP/1.1 200 OK
< Server: nginx/1.24.0
< Date: Fri, 03 Jan 2025 08:34:00 GMT
< Content-Type: text/html; charset=UTF-8
< Transfer-Encoding: chunked
< Connection: keep-alive
<
1* Connection #0 to host 172.18.91.76 left intact

2.GET 参数覆盖

C:\Users\gbb>curl http://172.18.91.76/ExternalVariableModification/doubleDollar2.php?test=get覆盖 -v
*   Trying 172.18.91.76:80...
* Connected to 172.18.91.76 (172.18.91.76) port 80
> GET /ExternalVariableModification/doubleDollar2.php?test=get瑕嗙洊 HTTP/1.1
> Host: 172.18.91.76
> User-Agent: curl/8.9.1
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 200 OK
< Server: nginx/1.24.0
< Date: Fri, 03 Jan 2025 08:38:17 GMT
< Content-Type: text/html; charset=UTF-8
< Transfer-Encoding: chunked
< Connection: keep-alive
<
get覆盖* Connection #0 to host 172.18.91.76 left intact

3.POST 参数覆盖

C:\Users\gbb>curl -X POST http://172.18.91.76/ExternalVariableModification/doubleDollar2.php -d "test=POST参数覆盖" -v
Note: Unnecessary use of -X or --request, POST is already inferred.
*   Trying 172.18.91.76:80...
* Connected to 172.18.91.76 (172.18.91.76) port 80
> POST /ExternalVariableModification/doubleDollar2.php HTTP/1.1
> Host: 172.18.91.76
> User-Agent: curl/8.9.1
> Accept: */*
> Content-Length: 21
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 21 bytes
< HTTP/1.1 200 OK
< Server: nginx/1.24.0
< Date: Fri, 03 Jan 2025 08:39:06 GMT
< Content-Type: text/html; charset=UTF-8
< Transfer-Encoding: chunked
< Connection: keep-alive
<
POST参数覆盖* Connection #0 to host 172.18.91.76 left intact

extract()

extract() 方法可以把数组里每个键提取出来作为变量名,值作为变量值,如果变量名重复 flag 参数默认情况下是 EXTR_OVERWRITE 覆盖原有变量的值,方法执行完成返回成功创建的变量 int 数。

extract(array &$array, int $flags = EXTR_OVERWRITE, string $prefix = ""): int

覆盖案例 extratct1.php。

<?php  
$size = "large";  
$var_array = array(  
    "color" => "blue",  
    "size" => "medium",  
    "shape" => "sphere"  
);  
echo $size . "\n";  
echo extract($var_array) . "\n";  
echo $size . "\n";  
echo "$shape\n$color";

运行后发现原来的 $size 变量的值被重新赋值成 medium,其他两个变量 $shape$color 也成功创建。

large
3
medium
sphere
blue

覆盖案例 extratct2.php。

<?php  
  
$test = "test变量的值";  
echo extract($_GET) . "\n";  
echo $test;

运行后也是一样的,只不过这次是通过 GET 参数来覆盖变量 $test

C:\Users\gbb>curl http://172.18.91.76/ExternalVariableModification/extratct2.php?test=篡改后test的值 -i
HTTP/1.1 200 OK
Server: nginx/1.24.0
Date: Fri, 03 Jan 2025 09:24:07 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive

1
篡改后test的值

parse_str()

parse_str() 方法可以将字符串转换成变量,$string 是要转换的字符串,&$result 在 PHP 8.0.0 之前是可选参数,作用是把转换的结果存到这个数组中。

parse_str(string $string, array &$result): void

覆盖案例 parse_str1.php。

$first = 1;  
echo $first . "\n";  
parse_str("first=2abc&last_array[]=ar&arr[]=b&array[]=3");
echo $first;

运行 parse_str 后,它把字符串 first 转换成变量并重新赋值 $first=2abc,成功覆盖原有 first 变量的值,而 last_array 变量不存在,是新创建了一个。

1
2abc
array(3) {
  [0]=>
  string(2) "ar"
  [1]=>
  string(1) "b"
  [2]=>
  string(1) "3"
}

这里对字符串解析还有两个规则,字符串中点和空格会被转换成下划线和字符串自动 URL 解码。比如有个变量带有下划线,仍然可以通过这个特性来覆盖它。

覆盖案例 parse_str2.php

<?php
$first_test = 1;  
echo "未覆盖原始值:" . $first_test . "\n";  
  
// 第一次覆盖  
parse_str("first.test=2abc");  
echo "第一次覆盖:" . $first_test . "\n";  
  
// 第二次覆盖  
parse_str("first test=2a%20bc");  
echo "第二次覆盖:" . $first_test . "\n";  
  
//第三次覆盖  
parse_str("first_test=2abc");  
echo "第三次覆盖:" . $first_test . "\n";

运行 parse_str2.php 后输出成功覆盖对应值。

未覆盖原始值:1
第一次覆盖:2abc
第二次覆盖:2a bc
第三次覆盖:2abc

mb_parse_str()

mb_parse_str() 是 mbstring 扩展中的函数,需要手动启用才能使用。mb_parse_str() 在用法和 parse_str() 一模一样,只是解析的字符串能支持更多的编码,可以理解为编码加强版。

register_globals 配置

register_globals 原本含义是用来自动注册全局变量,不过 PHP 从 2002.4.22 日发布的 4.2.0 版本开始,register_globals 的值默认就是 off 需要去 php.ini 手动设置 register_globals=on 开启,2012.3.1 日发布的 5.4.0 版本正式删除这个特性。从删除的版本日期来看,真实环境中遇到这种应用微乎其微,感觉只有 CTF 上才会出现为了考知识点才会遇见,仅做了解即可。

以前 http://www.example.com/?parm=test 前端传递 GET 参数 myvar,后端需要主动接受参数赋值给一个变量才能用这个变量。

$parm = $_GET['parm']; // parm 参数的值是字符串 test

如果你开启了自动注册全局变量这个配置,就由 PHP 自动帮你完成变量赋值操作,无需手动赋值可以直接使用。

echo $myvar; // parm 参数的值是字符串 test

哪些方法可以被自动注册替代呢?也就是 GET 参数、POST 参数、请求头这 3 个位置传入的数据都可以自动注册。

$_GET
$_POST
$_REQUEST
$_COOKIE
$_SESSION


------------下面这些方法到底在指定版本下能不能用还未知------------
https://www.php.net/manual/en/reserved.variables.php
$_COOKIE
$_SERVER
$_FILES
$GLOBALS['_GET']['参数名']
filter_input()
apache_request_headers()
getallheaders() # PHP7 才支持
php://input

import_request_variables()

2001.12.10 日发布的 4.1.0 版本中添加的特性,2012.3.1日发布的 5.4.0 版本正式删除此方法。

import_request_variables 也是个老古董,在当时来看很像 register_globals 替代方案,在如今的程序很少见的到,无需花精力在上面。

最近更新:

发布时间:

摆哈儿龙门阵