安全研究

前言

如今大量前端开发皆采用现代化前端框架,如Vue和React,这类框架对于XSS提供了自带的防御措施,大部分情况下基本完全杜绝了XSS,但是在某些特定的场合下,依旧存在XSS风险,为了避免在一些渗透测试,安全服务中在这类框架上花无用的挖洞时间,此文来探讨一下此类框架是如何防御XSS以及如何产生XSS的
介于此,此文将尽可能少的介绍Vue和React的基础知识,不了解的读者可以视情况先学习一下前置知识在看此文

Vue

防御

我们知道,Vue采用数据绑定的方式,将变量绑定到dom树上,如下所示

<div id="app">
    <p>{{ message }}</p>
</div>

<script>
    new Vue({
        el: '#app',
        data: {
            message: 'Hello Vue.js!'
        }
    })
</script>

el代表dom的查询表达式,查询id=app的元素
然后将 {{}}里的message变量渲染为Hello Vue.js! 字符串
几乎所有的开发都这么写,那么假如我们message变量可控会如何

<div id="app">
    <p>{{ message }}</p>
</div>

<script>
    new Vue({
        el: '#app',
        data: {
            message: '<svg/onload=alert(1)>'
        }
    })
</script>

这里message被我们硬编码为了html代码,当然正常代码会动态给message赋值,这里效果一样,不额外举例,现在我们看看渲染的结果
图片.png
可以看到,即使我们可控这些变量,开发者也没有做任何过滤,我们的xss payload依旧被转义为实体字符。
这就是vue底层做的事,vue对于变量绑定,底层其实是用了Javascript原生的innerText方法,而innerText默认会把所有html元素进行转义,由于innerText也是Js原生方法,当然正常情况不可能绕过

所以有时候,我们在某些系统里狂插payload,大家会发现没有一个弹窗,可能这不是开发者防御意识高,只是人家用了框架,攻击的方式错了

隐患1

如果开发者有将html代码作为值传入到变量中的需求呢,比如动态生成表格,对于一个大型框架来说,肯定有这种渲染标签的功能,
对于Vue来说,这个功能的指令叫V-html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Vue</title>
    <script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script>
</head>
<body>
<div id="app">
    <p v-html="message"></p>
</div>

<script>
    new Vue({
        el: '#app',
        data: {
            message: '<svg/onload=alert(1)>'
        }
    })
</script>
</body>
</html>

图片.png
很明显感觉到,v-html指令应该就是调用的document.innerHTML方法
如果开发者用v-html方法进行参数传递,并且参数可控,那么即可造成XSS

隐患2

vue用的最多的方法还是{{}},双大括号变量绑定,这个双大括号本质和 v-text指令是一样的,就是上面说的调用的innerText方法,对于一般的普通系统来说,几乎开发者全程使用{{}}进行参数传递,很少会额外用到v-html指令,那么如果不用v-html就没有常见的XSS问题了嘛?
对于一种很常见的XSS case,vue是没有做任何的防御的

<body>
<div id="app">
    <a :href="url">click me</a>
</div>

<script>
    new Vue({
        el: '#app',
        data: {
            url: "javascript:alert(111)"
        }
    })
</script>
</body>

a标签的属性xss,vue是没有任何防御措施的,我们只需要传入javascript:alert(1)即可触发XSS
除此以外
iframe的src属性,object标签的src属性都是可以触发XSS的


:href 其实是 v-bind:href的缩写
下面两个写法一样

    <a :href="url">click me</a>
    <a v-bind:href="url">click me</a>

隐患3

在javascript函数里面使用原生危险函数,比如innerHTML eval等,这个不再多说

React

React习惯用类语法来编写

隐患1

跟上诉href src一样,react不会对链接产生的xss进行过滤

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8" />
    <title>Hello React!</title>
    <script src="https://cdn.staticfile.org/react/16.4.0/umd/react.development.js"></script>
    <script src="https://cdn.staticfile.org/react-dom/16.4.0/umd/react-dom.development.js"></script>
    <script src="https://cdn.staticfile.org/babel-standalone/6.26.0/babel.min.js"></script>
</head>
<body>

<div id="example"></div>
<script type="text/babel">

    class WebSite extends React.Component {
        constructor() {
            super();

        }
        render() {
            return (
                <div>
                    <a href={this.props.site}>click me</a>
                </div>
            );
        }
    }
    ReactDOM.render(
        <WebSite site="javascript:alert(1)"/>,
        document.getElementById('example')
    );
</script>

</body>
</html>

隐患2

同样如上v-html,react也提供了一个渲染html的方法,不过这个办法名字比较吓人
dangerouslySetInnerHTML


<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8" />
    <title>Hello React!</title>
    <script src="https://cdn.staticfile.org/react/16.4.0/umd/react.development.js"></script>
    <script src="https://cdn.staticfile.org/react-dom/16.4.0/umd/react-dom.development.js"></script>
    <script src="https://cdn.staticfile.org/babel-standalone/6.26.0/babel.min.js"></script>
</head>
<body>

<div id="example"></div>
<script type="text/babel">

    class WebSite extends React.Component {
        constructor() {
            super();

        }
        render() {
            return (
                <div>
                    <p dangerouslySetInnerHTML={{__html:this.props.site}}></p>
                </div>
            );
        }
    }
    ReactDOM.render(
        <WebSite site="<svg/onload=alert(1)>"/>,
        document.getElementById('example')
    );
</script>

</body>
</html>

这个dangerouslySetInnerHTML跟vue的v-html比较类似

隐患3

同上,使用原生函数

总结

了解这些会有什么帮助

白盒审计

如果我们在对前端项目进行审核的时候,如果发现前端项目是用vue或者react进行编写的话,我们就可以只关注上面的危险函数关键字,避开其他的常规功能,比如全局搜索 如下关键字

:href
v-bind:href
:src
v-bind:src
v-html
------------------------------
href={
src={
dangerouslySetInnerHTML=

------------------------------
innerHTML
eval(
....

黑盒审计

当然,大部分时候我们是没有源代码的,我们一般面对的是一个webpack打包后的构建产物js,如果有sourcemap泄露的话,我们就可以当上面白盒进行审计,如果在没有sourcemap泄露的情况下,就必须硬怼黑盒环境了,当然黑盒会有一些技巧
一般来说,Vue和React不会单独使用,大部分与现在前端常用的UI框架结合使用,比如Vue+elementUI,React+Antdesign


这类UI框架有个很明显的特征,一般常用于CRM系统,就是什么后台管理系统这类的,像这个样子


图片.png
图片.png
图片.png
这种清一色的,左边上面菜单栏,中间内容,UI比较爽快的,大部分都是Vue+React的前端框架,所以在这里面疯狂扔<img src=#....是没有用的
正确的姿势是:
找输入链接的地方,比如网址,图片链接等输入框,插入javascript:alert(1)等,当然这里也有可能SSRF
找富文本,表格等,或者抓包发现往后端直接发送标签的地方,插入XSS poc
放弃掉其他功能,因为框架会做默认的转义

- Read More -
开源安全

前言


这几天,我审查了多个web框架的源代码,了解了几个不同语言的web框架的处理流程逻辑,并且由于公司业务的需要,我对几个go的web框架进行了review,并且在昨天晚上审查出了一个框架层面的任意url跳转漏洞

macaron


macaron是一个小型,高拓展,精巧的go web框架,目前github有3k的star,语法上类似于gin框架。于是我对其进行了审查,最终挖掘出一枚任意302跳转漏洞,虽然发现的漏洞危害不大,但是挖掘过程却值得拿来一说,

框架


先说下背景,对于web框架的审查,一般着重点有如下几个

  1. 静态文件


因为一般框架都会提供静态资源的访问功能,用于访问静态文件夹,如果这部分处理不当,可能会造成目录穿越从而任意文件读取的问题

  1. 302跳转


这也是本文的漏洞,按照标准,用户注册的路由必须是/结尾,比如
@app.route('/user/')
如果没有以斜杠结尾,很多框架也会在背后默默添加上斜杠以符合标准
所以如果用户访问一个没有以斜杠结尾的路径时,框架会默认跳转到斜杠结尾的路径。如果框架没有任何判断,直接跳转的的url为 用户访问url + '/' 就会产生漏洞
Django的CVE-2018-14574就是这么产生的https://xz.aliyun.com/t/3302

  1. 模板渲染


一般来说,模板渲染和框架逻辑会进行解耦,比如flask和jinja那样,但是不可否认的是,模板渲染也是框架的一部分,并且很多模板都会用eval这种动态解析的危险函数,如果处理不当,极可能造成rce漏洞


审计框架常见的漏洞类型就是这些,当然,不同的情况肯定有不同的处理,并不是说除了上诉漏洞框架就不会有别的漏洞了,具体还是要看开发这写法,这也是局限于小型框架,如果是django那种功能复杂的大型框架,那肯定有更多的攻击面挖掘点去挖掘

漏洞点


对于一般的MVC框架,开发者会注册好 路由和函数的对应关系,然后框架中有一个全局索引去保存好这些对应关系,当一个新请求来临时,服务器会把 这个请求的信息全部打包好,扔给框架进行处理。这些信息就是我们常说的Context


比如说路径 请求方式, 协议, 等等,框架拿到这些信息以后,就会根据http服务器提供的路径,去全局索引中查找该回调哪个函数。


而如果没有找到路径匹配的函数,并且没有其他合适中间处理middleware,框架就会返回404


而本文的漏洞出现在静态资源处理函数中,我们来跟着源码分析一下

漏洞代码

package main
import "gopkg.in/macaron.v1"
func main() {
    m := macaron.Classic()
    m.Get("/", func(ctx *macaron.Context) string {
        return "Hello world!"
    })
    m.Use(macaron.Static("static"))
    m.Run()
}


payload放在了文末,建议读者先不直接看payload,看完代码分析,试试自己能不能想出payload

分析


git clone [https://github.com/go-macaron/macaron](https://github.com/go-macaron/macaron)


在本框架中,静态资源处理是一个中间件middleware
m.Use(macaron.Static("static"))


我们使用Goland来分析,右键Staic -> Go To -> Declaration 进入Static的函数声明

// Static returns a middleware handler that serves static files in the given directory.
func Static(directory string, staticOpt ...StaticOptions) Handler {
    opt := prepareStaticOptions(directory, staticOpt)

    return func(ctx *Context, log *log.Logger) {
        staticHandler(ctx, log, opt)
    }
}


由于我们使用的默认options,prepareStaticOptions函数并没有其他有用的信息,我们继续跟进staticHandler函数,看参数名字其实不难猜出,ctx就是上面讲的Context(请求信息上下文)log代表日志一般不用看,opt是选项


staticHandler就是导致漏洞的关键函数了,我们详细分析一下它,

func staticHandler(ctx *Context, log *log.Logger, opt StaticOptions) bool {

    file := ctx.Req.URL.Path
    // if we have a prefix, filter requests by stripping the prefix

    f, err := opt.FileSystem.Open(file)
    if err != nil {
        return false
    }
    defer f.Close()
    fi, err := f.Stat()
    if err != nil {
        return true // File exists but fail to open.
    }
    // Try to serve index file
    if fi.IsDir() {
        // Redirect if missing trailing slash.
        if !strings.HasSuffix(ctx.Req.URL.Path, "/") {
            http.Redirect(ctx.Resp, ctx.Req.Request, ctx.Req.URL.Path+"/", http.StatusFound)
            return true
        }
    }
    http.ServeContent(ctx.Resp, ctx.Req.Request, file, fi.ModTime(), f)
    return true
}


由于篇幅原因,我把只贴出了staticHandler产生漏洞的代码,没放出其余对漏洞产生无影响的代码。


不难看出,file就代表了我们请求的路径。
我们访问的HTTP包如果是POST //123./ HTTP/1.1
那么file就是//123./


opt.FileSystem在外部代码中查看得出指向http.FileSystem,这是一个go的内置包,可以打开文件和文件夹对象,所以后续代码用fi.IsDir()来判断是否打开的是文件夹。


这里就可以看出,这些代码是用来判断,用户访问的是静态目录下的一个文件还是文件夹,因为静态目录下的所有文件和文件夹都是可访问的。
然后我们仔细看看下面这几行代码

if !strings.HasSuffix(ctx.Req.URL.Path, "/") {
            http.Redirect(ctx.Resp, ctx.Req.Request, ctx.Req.URL.Path+"/", http.StatusFound)
            return true
        }


这几行代码的作用是,如果用户访问的是一个文件夹,但是访问路径不是以/结尾,就直接给路径加个/ 然后重定向。
看上去代码并没有任何问题,如果真要造成302任意跳转,比如跳到baidu.com,我们得在静态目录下新建一个叫 baidu.com 的文件夹,然后访问
[http://127.0.0.1:4000//baidu.com](http://127.0.0.1:4000//baidu.com)才可以.
包括我在第一次审计的时候也没有觉得有问题,各位读者如果看看到了这里,可以自己思考一下怎么利用。

利用












































公布答案
当访问 [http://127.0.0.1:4000//evoa.me%2f..](http://127.0.0.1:4000//evoa.me%252f..)即可跳到//evoa.me


我们来分析一下这个payload,由于路径是 .. 结尾,http.FileSystem会认为他是一个路径,fi.IsDir()为真,并且路径并不是/结尾,if !strings.HasSuffix(ctx.Req.URL.Path, "/")为true,继续进入if语句,最后返回的location为
Location: //evoa.me/../成功跳转

小坑


这里有个小坑,如果是在Chrome浏览器验证的话,
下面的payload都是无法成功的
[http://127.0.0.1:4000//evoa.me/..](http://127.0.0.1:4000//evoa.me/..)
[http://127.0.0.1:4000//evoa.me/%2e.](http://127.0.0.1:4000//evoa.me/%252e.)
[http://127.0.0.1:4000//evoa.me/%2e%2e](http://127.0.0.1:4000//evoa.me/%252e%252e)
[http://127.0.0.1:4000//evoa.me/.%2e](http://127.0.0.1:4000//evoa.me/.%252e)


具体原因是chrome会对访问路径进行标准化,/..会自动删去,不会发到后端,这里要感谢 @F1sh师傅的解答
而如果对 / 进行编码 %2f,chrome就不会自动标准化,就可以复现这个漏洞了

后话


Issue地址:https://github.com/go-macaron/macaron/issues/198
CVE:在考虑要不要申请,感觉很垃圾

CVE-2020-12666 真香

当框架没有找到任何匹配的controller时,框架会认为访问的路径是一个静态文件,所以试图去查看静态文件夹下是否有对应的文件
大概逻辑是


当访问 [http://127.0.0.1:4000//evoa.me%2f..](http://127.0.0.1:4000//evoa.me%252f..)即可跳到//evoa.me

- Read More -
安全研究

前言

pty(pseudo terminal)又称伪终端,大家比较熟的可能是tty (Teletype),也就是计算机的终端设备。这篇文章就是阐述何为pty,pty本质是什么,为什么我们渗透的时拿到shell后需要获取pty,没有pty为什么处处受限。pty和tty的关系又是什么

tty

在说明pty之前,需要介绍一下tty,tty可以直接理解为终端,而介绍tty需要大概说明一下计算机的历史,tty全称为Teletypes(电传打字机),是通过串行线用打印机键盘通过阅读和发送信息的东西,后来这东西被键盘和显示器取代,所以现在叫终端比较合适。
下面是早期计算机通过电传打字机交互的模型

UART 驱动
如上图所示,物理终端通过电缆连接到计算机上的 UART(通用异步接收器和发射器)。操作系统中有一个 UART 驱动程序用于管理字节的物理传输。
线规
上图中内核中的 Line discipline(行规范)用来提供一个编辑缓冲区和一些基本的编辑命令(退格,清除单个单词,清除行,重新打印),主要用来支持用户在输入时的行为(比如输错了,需要退格)。
TTY 驱动
TTY 驱动用来进行会话管理,并且处理各种终端设备。

UART 驱动、行规范和 TTY 驱动都位于内核中,它们的一端是终端设备,另一端是用户进程。因为在 Linux 下所有的设备都是文件,所以它们三个加在一起被称为 "TTY 设备",即我们常说的 TTY。

再来看一个linux控制台的模型

虽然这个模型看上去没什么问题,但随着linux的发展,终端固定再内核层过于僵化,某些进程需要自主实现一个终端模拟器,比如ssh,xterm。而tty完全由内核接管。用户态无法使用tty的功能,于是linux提出将终端仿真移动至用户态,这就是pty的由来

当创建一个伪终端时,会在 /dev/pts 目录下创建一个设备文件:

如果是通过 PuTTY 等终端仿真程序通过 SSH 的方式远程连接 Linux,那么终端仿真程序通过 SSH 与 PTY master side 交换数据。

线规

线规(line discipline),线规是终端(tty)子系统的一部分。线规将底层设备驱动程序代码与高层通用接口例程(比如read,write等系统调用)粘合在一起,并负责实现与设备关联的语义。
例如,标准线规会根据类Unix系统上终端的要求,处理从硬件驱动程序和写入设备的应用程序接收到的数据。在输入时,它处理特殊字符,例如中断字符(通常为Control-C)以及擦除和杀死字符(通常分别为backspacedelete和Control-U),并且在输出时,它将所有LF字符替换为CR / LF序列。
通俗来讲,线规会把用户输入的某些特殊字符替换成真正用户想表达的语义,比如退格键代表删除一个字符。而不是输入一个退格键的ascii码进去。
所以,为什么我们在渗透的时候弹回来的shell,如果直接输入退格键会出现乱码,就是因为退格键没有经过线规的处理,被直接当做了一个字符。
PS:线规处于内核层

pty

如上所说,为了使应用程序能有效使用终端功能,操作系统提供了伪终端功能。那pty的实现是怎么样的呢
pty由master和slave两端构成,在任何一端的输入都会传达到另一端。与tty不同,系统中并不存在pty这种文件,它是由pts(pseudo-terminal slave)和ptmx(pseudo-teiminal master)两种设备文件来实现的。

pts

(pseudo-terminal slave)即伪终端的slave端。在Linux的/dev/pts/文件夹下有对应设设备文件。
我们可以通过tty命令查看当前用户的登录终端,如下图所示:

ubuntu@VM-32-73-ubuntu:/dev$ tty
/dev/pts/1

当我们设备文件/dev/pts/1进行输出时,屏幕上会显示相应输出:

ubuntu@VM-32-73-ubuntu:/dev$ echo hello >/dev/pts/1
hello

倘若访问别的slave文件,如/dev/pts/2,则会返回权限不足错误:(root例外)

ubuntu@VM-32-73-ubuntu:/dev$ echo hello >/dev/pts/2
-bash: /dev/pts/2: Permission denied

所以,如果我们拥有root权限,我们理论上可以控制任何伪终端的输出

ptmx

(pseudo-terminal master)
ptmx是伪终端的master端。在/dev下仅有2个ptmx文件,其信息如下:

ubuntu@VM-32-73-ubuntu:/dev$ ll /dev/ptmx
crw-rw-rw- 1 root tty 5, 2 Jan 16 16:38 /dev/ptmx
ubuntu@VM-32-73-ubuntu:/dev$ ll /dev/pts/ptmx
c--------- 1 root root 5, 2 Mar 17  2018 /dev/pts/ptmx

讲讲现象背后的故事
当ubuntu系统创建一个新的terminal时(比如上面的pts/1)
首先执行ptm = open('/dev/ptmx',...)操作
接下来fork(),然后child进程将打开'/dev/pts/1',dup2到0,1和2句柄上,随后执行execl启动一个shell.
pts = open('/dev/pts/1',...);
dup2(pts, 0); // 对应lib库中stdin
dup2(pts, 1); // 对应lib库中stdout
dup2(pts, 2); // 对应lib库中stderr
close(pts);
execl("/system/bin/sh", "/system/bin/sh", NULL);
// 这样sh输入数据将全部来自pts,
// sh的输出数据也都全部输送到pts,也就直接送到了打开ptmx的新terminal中.

新terminal将启动GUI,捕获按键数据,然后写入ptm,这样pts将收到数据,进而sh将从stdin中获得数据,
于是sh将作进一步运算,将结果送给stdout或stderr,进而送给pts,于是ptm获得数据,然后terminal的GUI
将数据显示出来.

terminal捕获到key按键值 <--> ptm <--> pts/1 <--> stdin <--> shell读到数据
shell数据结果 <--> stdout <--> pts/1 <--> ptm <--> terminal显示

因为是master - slaver,所以ptm只有一个,pts可以有多个
我们用一个ssh的图来看

+----------+       +------------+
 | Keyboard |------>|            |
 +----------+       |  Terminal  |
 | Monitor  |<------|            |
 +----------+       +------------+
                          |
                          |  ssh protocol
                          |
                          ↓
                    +------------+
                    |            |
                    | ssh server |--------------------------+
                    |            |           fork           |
                    +------------+                          |
                        |   ↑                               |
                        |   |                               |
                  write |   | read                          |
                        |   |                               |
                  +-----|---|-------------------+           |
                  |     ↓   |                   |           ↓
                  |   +--------+   +-------+    |       +-------+  fork   +-------------+
                  |   |  ptmx  |<->| pts/0 |<---------->| shell |-------->| tmux client |
                  |   +--------+   +-------+    |       +-------+         +-------------+
                  |   |        |                |                               ↑
                  |   +--------+   +-------+    |       +-------+               |
                  |   |  ptmx  |<->| pts/2 |<---------->| shell |               |
                  |   +--------+   +-------+    |       +-------+               |
                  |     ↑   |  Kernel           |           ↑                   |
                  +-----|---|-------------------+           |                   |
                        |   |                               |                   |
                        |w/r|   +---------------------------+                   |
                        |   |   |            fork                               |
                        |   ↓   |                                               |
                    +-------------+                                             |
                    |             |                                             |
                    | tmux server |<--------------------------------------------+
                    |             |
                    +-------------+

需要注意的是,由于pts是slave端,所以不支持一对多,如果我们在linux中开启两个终端分别是pts1 和 pts2
如果我们再pts2中执行 cat /dev/pts/1命令,然后我们在pts1终端中输入字符,可以发现一部分字符会回显再pts1端上,另一部分的字符会会显在pts2上。我画个图就很好理解为什么了
图片.png
当我们在pts1中输入数据时,输入流从ptmx传递给pts1在传递给bash,bash会把用户输入原样返回给输出流。这时候pts1接收到bash返还给的输出,但此时有两个应用程序在等待pts1的返回。一个是ptmx,一个是pts2下的cat进程(其实应该是pts2下bash的子进程)。于是此时就发生了数据争夺。linux内核调度器根据当时情况随时都会将他们中的一个调出或者调入,因此数据就出现了一部分被送到了pts/2的cat命令,另一部分被送到了pts1的shell,

终端与伪终端的区别

至此我们可以得出这样的结论:现在所说的终端已经不是硬件终端了,而是软件仿真终端(终端模拟软件)。
关于终端和伪终端,可以简单的理解如下:

  • 真正的硬件终端基本上已经看不到了,现在所说的终端、伪终端都是软件仿真终端(即终端模拟软件)
  • 一些连接了键盘和显示器的系统中,我们可以接触到运行在内核态的软件仿真终端(tty1-tty6)
  • 通过 SSH 等方式建立的连接中使用的都是伪终端
  • 伪终端是运行在用户态的软件仿真终端

制作rootkit

上一篇文章留下来的坑https://evoa.me/index.php/archives/64/
我们试试能不能制作一个rootkit,负责记录所有pty的输入输出,这样当我们拿下一台linux主机之后。我们就可以监控所有终端的输入输出。包括其他用户ssh连上来的和在此机器上通过ssh连别的机器的所有输入输出。
但是可惜的是,我搜遍了几乎所有,都没有找到一个完美的解决方案,唯一能让我稍微满意的,就是通过strace命令监控io系统调用。
于是我写了一个很丑的脚本,勉强能完成上诉需求。
怎么实现呢,原理很简单,一般来说pty是由一个进程来控制的,那么我们只要知道这个进程的进程id(pid),那么通过strace获取这个进程的io系统调用,write(1)代表输出,read(0)代表输入(文件描述符),然后通过正则获取参数,就可以获取pty的所有系统调用了
优点:

  1. 可以获取连接到此机器的所有伪终端的输入输出。包括不限于telnet,ssh,本地终端
  2. 可以获取到连接到此机器的基础上,在通过telnet,ssh等连接到别的机器时所有的输入输出(可无限循环)
  3. 可以获取到不回显至终端的输入(比如sudo时输入的密码,mysql连接时的密码)

缺点:

  1. 必须拥有root权限,否则只能获取和当前用户同一pty的进程的输入输出
  2. 严重依赖ptrace系统调用和strace命令
  3. echo 0 > /proc/sys/kernel/yama/ptrace_scope,当然root权限可以更改此选项

由于代码过丑,存在很多bug,我暂时就不贴出来和放在github了,等有时间写个go版本的用原生系统调用实现

大概说一下我的实现细节:

  1. 主程序第一次运行时,执行ps -ef获取当前系统所有pty进程,
  2. 删除与自身pty一样的进程
  3. 然后使用多进程或多线程运行strace命令依次获取这些进程的系统调用内容。
  4. 用正则获取所有的输入和输出,筛选(这部分很细节)
  5. 主程序运行第一次ps -ef以后会轮询ps -ef,如果发现新产生的pty进程,继续3步骤
  6. 把输入输出输出到文件或终端

说起来很容易,但是很多细节很麻烦

  1. 进程中还会有子进程,子进程还有子进程,会出现子进程退出主进程没退出或者主进程退出子进程还没从全局列表删去这些问题
  2. strace可以自动追踪子进程,但是可能和主程序的轮询冲突。
  3. trace附加到的进程无法获取父进程的输入输出,strace先附加进程,然后这个进程再产生子进程的话,strace可以追踪到,但是如果strace附加之前这个进程就已经产生的子进程,strace附加后无法获取到。
  4. write系统调用除了输出到1文件描述符会实现回显,输入到0标准输入也有回显,2标准错误也有回显,还可以直接输出到/dev/pts/x 还可以输出到/dev/tty
  5. dup2系统调用会复制一个文件描述符,我们需要追踪这个系统调用,然后判断复制的文件描述符是否是标准输入 标准输出 标准错误。需要实现一个全局列表记录
  6. close会关闭一个文件描述符,后续可能会有open或openat系统调用打开文件描述符,可能前一秒这个文件描述符是存在的,后一秒就被关闭了,再后一秒又被重新打开了并且指向发生了改变,这些都需要进行追踪

所以。。。具体实现细节过于麻烦,这也导致了我写的很难受

后话

如果想要我的残次品脚本的也可以私我。功能确实实现了,就是一堆bug

参考:
https://www.jianshu.com/p/11c01003211b
https://en.wikipedia.org/wiki/Line_discipline
http://www.linusakesson.net/programming/tty/
https://segmentfault.com/a/1190000009082089
https://www.cnblogs.com/sparkdev/p/11460821.html
https://blog.csdn.net/zhoucheng05_13/article/details/86510469

- Read More -
安全研究

前言

在做分析二进制文件的时候,难免会遇到需要在程序输入处输入一些不可显字符,一般我们会通过pwntools进行解决

from pwn import *

con = process('ret2lib')
con.recvuntil("input:")

con.send("\x01\x01\x00\x01")
con.interactive()

但是当脚本存在一些问题,我们需要通过gdb调试时,在程序输入中输入不可显字符就较为麻烦。于是有了这篇文章

正常方法

pwn师傅给我的方案是,首先输入正常字符,输入后,找到字符串地址,通过GDB 命令 set xxx=xxx对内存处进行更改
图片.png

网上找了一下GDB set命令,大部分教程都是修改整型

(gdb) set {unsigned int}0x8048a51=0x0

对于字符串的修改却没找到中文资料
于是我稍微仿照试了一下啊,最终发现

(gdb) set {char [6]}0x8048a51="12345"

可行,需要注意,[]数值包括了0,所以需要比字符串常量多1。
并且类型不能使用{char *},否则 对应地址处会继续存放一个字符串指针,而不是字符串值,如下图

图片.png
图片.png
------------------------------------------------------------------------------------------------------

图片.png
图片.png

此外,我在网上还搜到一个人写的GDB插件,不过我下载下来以后使用不了,看源码发现是使用了GDB的call命令重定向了文件描述符(call 命令还有这个功能?不太懂)没有深究
https://www.jianshu.com/p/78e77277ebb5

错误方法

但是一开始我用的方法不是PWN师傅教我的,当时,我自己的理解是每个文件下都有3个文件描述符,
0 -> stdin(标准输入)
1 -> stdout(标准输出)
2 -> stderr (标准错误)
而且在Linux中,万物皆文件,这三个文件描述符分别存储在 /proc/{pid}/fd/ 下
那我直接往 标准输入里面写数据不就可以了吗
我的做法如下
demo.c

#include<stdio.h>
#include <unistd.h>
int main(){
    pid_t pid = getpid();
    char s[100];
    printf("pid of this process:%d\n", pid);
    printf("please input string:\n");
    scanf("%s",s);
    printf("U input String is :%s",s);
    return 0;
}

运行过程
图片.png

这是我键盘输入的123456798,那如果我往标准输入写数据呢

下面是我的尝试
图片.png
keyboard input :123456是我在键盘上打出来的字符串,很明显可以看到,虽然我们往对应进程的标准输入描述符中写入的数据被打印到了终端上,但是程序进程的输出却告诉它并没有接收到这些数据。而我用键盘继续输入的字符串才真正被程序接收
PS: 由于scanf函数读取到空格会停止,所以keyboard后面的字符串并没有被接受

原因

虽然往标准输入写数据 理论上听上去没什么问题,但是结果告诉我们并不能成功,网上搜索的时候中文搜索引擎并没有相关的结果,但是谷歌一下就找到了原因

https://serverfault.com/questions/178457/can-i-send-some-text-to-the-stdin-of-an-active-process-running-in-a-screen-sessi#
中文翻译一下大概就是
提问者提出了linux服务器终端有个任务,怎么样才能写脚本代替手工往这个终端任务的标准输入写数据

而下面的回答就是,往/proc/{pid}/fd/0 写入数据只会回显到tty上,并不会被程序接受
原因是 正常的写文件操作并不能被程序读取,需要以一种特殊的方式发送输入文本以供过程读取。通过常规文件write方法发送输入文本将不会导致进程接收文本。这是因为这样做只会附加到该“文件”,而不会触发进程读取字节。
为了触发该过程以读取字节,必须对要发送的每个单个字节IOCTL执行类型的操作TIOCSTI。这会将字节放入进程的标准输入队列中。
我的理解是,这种输入不是正常文件读取,而是一种流式传输,所以我上面的粗暴写文件方法是无效的。

那怎么进行流式传输呢,系统肯定提供了对应的系统调用呀
系统调用 ioctl
图片.png

C demo

根据描述,ioctl是控制文件描述符 I/O通道的函数,答者根据这个系统调用写了一个小demo来往标准输入里面写数据
PS: 往其他文件的标准输入写数据需要root权限

对应的demo
https://raw.githubusercontent.com/grawity/code/master/thirdparty/writevt.c

/*
 * Mostly ripped off of console-tools' writevt.c
 */

#include <stdio.h>
#include <fcntl.h>
#include <termios.h>
#include <sys/ioctl.h>
#include <unistd.h>

char *progname;

static int usage() {
    printf("Usage: %s ttydev text\n", progname);
    return 2;
}

int main(int argc, char **argv) {
    int fd, argi;
    char *term = NULL;
    char *text = NULL;

    progname = argv[0];

    argi = 1;

    if (argi < argc)
        term = argv[argi++];
    else {
        fprintf(stderr, "%s: no tty specified\n", progname);
        return usage();
    }

    if (argi < argc)
        text = argv[argi++];
    else {
        fprintf(stderr, "%s: no text specified\n", progname);
        return usage();
    }

    if (argi != argc) {
        fprintf(stderr, "%s: too many arguments\n", progname);
        return usage();
    }

    fd = open(term, O_RDONLY);
    if (fd < 0) {
        perror(term);
        fprintf(stderr, "%s: could not open tty\n", progname);
        return 1;
    }

    while (*text) {
        if (ioctl(fd, TIOCSTI, text)) {
            perror("ioctl");
            return 1;
        }
        text++;
    }

    return 0;
}

编译完以后,只需要执行

writevt /proc/{pid}/fd/0 "you text"

图片.png

但是这个程序有个小bug,很明显,这个程序是把第二个命令行参数当作文本,第一个参数当成描述符。但是如果我们需要输入特殊字符,比如回车,我们一般会这么输入
图片.png
但是程序显示参数过多,因为回车会被当成命令行分隔符,123n456,运行结束后123会被当成第二个命令行参数,456会被当成第三个命令行参数,所以我们无法通过这个输入某些特殊字符。

这肯定不是我们想要的,但修改C代码稍微有点繁琐,好在,另一个回答提供了python demo

python demo

import fcntl
import sys
import termios

with open('/dev/tty1', 'w') as fd:
    for char in "ls -la\n":
        fcntl.ioctl(fd, termios.TIOCSTI, char)

稍微改成上面的形式就是

import fcntl
import sys
import termios

with open(sys.argv[1], 'w') as fd:
    for char in sys.argv[2]:
        fcntl.ioctl(fd, termios.TIOCSTI, char)

有了python就好办事了,我们可以规定命令行传入的特殊字符会编码一次,而程序中再解码一次即可

#!/usr/bin/python
# writev.py

import fcntl
import sys
import termios

with open(sys.argv[1], 'w') as fd:
    for char in eval("'"+raw_input()+"'"):
        fcntl.ioctl(fd, termios.TIOCSTI, char)

图片.png
PS:由于bash中会转义反斜杠,所以这里需要双反斜杠

成功的把数据输入到了进程的标准输入中,进程也成功接收到了数据

tty

触类旁通,我们知道linux中,每个终端就代表了一个tty,tty也是一个文件描述符,既然我们能控制输入输出,理论上就应该也能控制tty,
图片.png
确实如此,我们可以模拟tty的键盘输入,往tty里面写数据,但是如果要获取tty的标准输出,和获取正常输入的标准输入呢?

这里埋个坑。这方面的资料真的太少了,google搜到的资料也太杂了,暂时也没什么思绪,准备明天去看看pwntools的源码,先鸽了,一定更新,下次一定

- Read More -
Android

前言

本来说暑假学一点安卓,结果看来看去还是被Root吸引了,《第一行代码》看了一小部分,然后凭着自己的理解,大概明白了Root的原理。这篇文章就是就当培养兴趣,也没啥技术干货,当故事讲。
因为笔者水平有限,如果这篇文章有技术错误,可以在下方指出,我会及时更改

Android系统

众所周知,Android系统是在Linux的基础上开发的,安卓本质上就是Linux的二次开发,用的依旧是Linux内核,只不过安卓封装了一层。对于Linux的底层,/为根目录的文件系统,/bin目录下的cd ls su命令,都依旧存在在手机中,只不过安卓做好了封装,对于用户是不可见的,就像安卓机上系统不会给你一个终端让你玩,当然很多第三方app有这种功能(要root才能使用)

在我还没学安卓的时候我一直有一个疑惑,为什么安卓一般都用Java写。C才应该是跟系统打交道的语言啊。要回答这个问题我首先会介绍一些系统知识,以防止不太了解的同学听不懂

可执行文件

一般来说,我们提到的应用软件都指的是可执行文件,在Windows上,这个文件是EXE,在Linux上,一般指的是ELF,操作系统提供了对可执行文件的支持,可执行文件不需要任何其他环境就可以执行。所以我们编写软件,也一般指的是编写EXE或ELF文件。
对于exe或者elf,C语言家族肯定是老大哥,c的编译默认就是生成可执行文件,编译完成只需要双击就可以运行。(这里不讨论系统库(dll或者so))而对于其他热门语言来说,首先很大一部分不支持生成可执行文件,其次对于一些和系统打交道的底层细节处理上无法实现或者很难实现(比如我要给我的硬件发送硬件信号,读取内存为0x80000的内容)

系统调用

为什么会这样,就要介绍系统调用了,这方面内容比较复杂,详细的话可以百度,我这里简单介绍一下。
图片.png

我们把上图的用户当作是我们写的程序,对于操作系统来说,操作系统承当负责用户与计算机硬件中间的翻译者,操作系统提供了很多名为系统调用的函数,对于操作系统来说,程序不需要也不允许直接操控硬件,一切直接与硬件交互的事情都交给操作系统来做,这就是内核态。

程序只需要使用系统调用就可以完成大多数功能。这就是用户态。

因为对于硬件来说,硬件的控制过于麻烦,不可能每个程序员都需要深入了解硬件的控制才能写程序,比如程序员想读取一个文件,需要先判断文件在哪个扇区磁道,然后编程序向硬盘发送对应硬件信号,读取xx扇区xx磁道。估计世界上没几个人想当程序员了。而如果拥有操作系统,只需要使用操作系统提供的系统调用函数 open和read函数,就可以轻松读取文件。
比如C语言中最常见的printf函数,其实这个函数在系统调用函数的基础上继续做了封装,最底层的系统函数是write函数。

C语言

扯远了,继续谈为什么系统应用常用c语言来做,如上所说,要实现最所有基本的功能,就需要程序能够调用系统提供的系统调用函数。系统调用函数本质就是在内存中存在的一串汇编代码。所以理论上只需要知道这串代码的起始地址,就可以调用系统调用函数。而对于C语言,首先默认支持调用系统调用函数,原因是C的函数调用默认就是指针(内存地址)调用。
而对于其他语言,内存地址大部分都是被屏蔽的,所以我们无法通过指针调用系统原始的系统调用。虽然一般这些语言会在底层封装好一些常见的系统调用提供使用,但封装肯定会遗失一部分功能,对于一些底层功能,用高级语言就难以实现,再就是之前说的,很多语言不支持生成exe,而且还需要运行库。所以一些时候,用其他语言直接做软件比较麻烦,当然也能做。

Android 系统

继续回到安卓,安卓的开发者可能觉得用C开发手机应用比较麻烦,于是用了Java把常用系统调用封装了一遍,并且屏蔽了底层的所有细节。相当于安卓开发者用Java在linux的基础上在开发了一个新系统,而这个新系统提供的"系统调用"都是Java编写的,所以app开发者也必须用Java去调用这些"系统调用"函数,所以安卓就用Java开发最方便。如果当初开发者用Python封装这些系统调用函数,可能安卓就要用python写了。

我们可以理解安卓系统是建立在Linux上的一个沙盒,底层的细节都被屏蔽。
说了这么久还没说root的本质,其实手机root就是获取root权限,没错就是Linux最高权限用户root的权限。

但是安卓的开发者在开发的时候就对安卓系统做了严格的安全策略,只有内核部分权限具有root权限,一切用户态应用都是普通用户。也就是说我们编的程序在安卓上都是普通用户权限。
对于Linux,如果我们要成为root,大家都会想到使用su命令,然而安卓开发者也想到了这个命令,他们对su命令进行了更改,加入了一行逻辑,如果该用户不是root权限,那么不允许使用su命令。
这就形成了一个逻辑闭环
想成为root -> 使用su命令 -> 必须具有root权限 -> 想成为root

利用漏洞ROOT

虽然安卓开发者的想法天衣无缝,按照系统规则,程序确实无法拥有root权限。但是早期的安卓系统存在许多漏洞,学过提权原理的同学应该不难理解,root的本质就是提权,提权的本质一般就是利用高权限程序,劫持高权限程序代码,执行任意代码,这些代码就具有了高权限。提权就成功了。
比如pwn里最经典的栈溢出,如果一个内核程序具备root权限,同时存在栈溢出漏洞,那么就可以劫持执行任意代码,对于安卓的root,一般方法就是把su文件替换为没有限制的su文件,当然替换su文件需要有root权限。而我们劫持完root权限程序以后,就可以随意替换。这样以后所有程序,只需要运行一下su文件,就具有了root权限。

当然安卓这么多版本,具体的漏洞原理我肯定不会讲,但是安卓发展这么久了,就跟软件一样,漏洞越来越难挖。以至于现在新版android已经几乎没有办法通过漏洞root了

boot和recovery root

boot是开机启动时要执行的一段代码,recovery是一个与安卓系统平行的一个小工具系统,类似Windows PE
具体原理就是,既然安卓系统层面上无法root,我就绕过系统,在不加载系统的时候先把su文件替换了。
具体可以看看http://blog.sina.com.cn/s/blog_54b537150102wl24.html
这篇文章

物理root (自己想的)

学过逆向的肯定知道一句话,没有破不了的软件,只有不值得破的软件
我觉得在安卓身上也是,虽然软件层面不可破,但是毕竟自己的手机在自己手里啊。
既然我只需要替换一个su文件就可以root,su文件无法更改是操作系统的限制,那我能不能把手机磁盘取下来,用其他硬件设备或者其他手机连上去,然后放回原来的手机。
就跟windows一样,我一直有个想法,如果windows密码忘记了,如果密码是一个文件存储的,那能不能把磁盘取下来,放到别的电脑上,把密码文件改了,再插回去,毕竟电脑手机在自己手里。IOS越狱也同理。
虽然理论逻辑听上去没有什么问题,但是好像这方面的资料找不到,网上也没有相关信息,这个只能是猜测。
但是自己想一下,如果硬盘是微电子嵌入在主板中的可能就取不下来了。或者操作系统会对一些文件做签名校验?不过这样应该只会增大破解难度。

可惜,这个只是猜测,我没找到比较详细的资料(可能我搜索关键字不太对?)如果有师傅了解这个的,欢迎和我探讨。不甚感激

- Read More -
This is just a placeholder img.