代码审计 - Web 应用常见漏洞
无非是各个框架或者类库的使用方法不一样,但是漏洞基础原理是相同的,这需要针对性对每个不同的语言的应用使用的错误方式进行总结。
目录
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()
如果只能应用固定命令,把输入作为参数附加到命令后面,那就看见 GTFOBins 或 LOLBAS 有没这些程序利用方式。
远程代码执行
输入的数据能够直接走到系统中指定代码的方法里。
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 替代方案,在如今的程序很少见的到,无需花精力在上面。
最近更新:
发布时间: