Durzosploit presented in a Core Impact webcast!

A small tool I created called Durzosploit will be presented on December 3rd by Core Impact in one of their webcast. Durzosploit is a really small console tool you can use to generate Javascript exploits.

Additionally I have no idea if people from Core Impact follow this blog, but if they do this is the URL to my latest SVN project called ErebosHacking which contains the latest version of both Browser Rider and Durzosploit: svn://engineeringforfun.com/svn/erebos-hacking/

Share This Post

Microsoft Windows Media File Handling Code Execution (MS09-038)

In this small post I will explain my analysis of Microsoft security patch MS09-038. As it is the first time that I even reverse anything, please excuse me in advance for all the things I may get wrong in that blog post and feel free to correct me or add anything through the comments.

On the 11th of August, Microsoft published a security patch fixing an integer overflow vulnerability discovered by Vinay Anantharaman in Windows Media in AVI files. That vulnerability is present within Avifil32.dll.

The vulnerable decompiled function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
signed int __stdcall sub_73AAA586(int a1, int a2, int a3)
{
  DWORD v3; // ecx@1
  int v4; // edi@1
  signed int result; // eax@2
  int v6; // esi@3
  HGLOBAL hMem; // eax@5
  LPVOID LP_ret_GlobalLock_2; // ebx@7
  unsigned int v9; // eax@1
  HGLOBAL v10; // eax@5
  DWORD v11; // ST04_4@5
  HGLOBAL v12; // eax@5
  LPVOID LP_ret_GlobalLock_1; // eax@7
  int v14; // eax@9
 
  v4 = a3;
  v9 = *(_DWORD *)(a3 + 4);
  v3 = v9 + 8;
  a3 = v9 + 8;
  if ( v9 + 8 < v9 )
    return -2147205017;
  v6 = a1;
  if ( *(_DWORD *)a1 )
  {
    if ( v3 + *(_DWORD *)(a1 + 4) < v3 )
      return -2147205017;
    v10 = GlobalHandle(*(LPCVOID *)a1);
    GlobalUnlock(v10);
    v11 = a3 + *(_DWORD *)(v6 + 4);
    v12 = GlobalHandle(*(LPCVOID *)v6);
    hMem = GlobalReAlloc(v12, v11, 0x2002u);
  }
  else
  {
    hMem = GlobalAlloc(0x2002u, v3);
  }
  LP_ret_GlobalLock_1 = GlobalLock(hMem);
  LP_ret_GlobalLock_2 = LP_ret_GlobalLock_1;
  if ( !LP_ret_GlobalLock_1 )
    return -2147205017;
  v14 = *(_DWORD *)(v6 + 4);
  *(_DWORD *)v6 = LP_ret_GlobalLock_2;
  *(_DWORD *)((char *)LP_ret_GlobalLock_2 + v14) = *(_DWORD *)v4;
  *(_DWORD *)(LP_ret_GlobalLock_2 + *(_DWORD *)(v6 + 4) + 4) = *(_DWORD *)(v4 + 4);
  sub_73AAFE2D(a2, *(_DWORD *)(v4 + 12), 0);
  if ( sub_73AAFDAF(a2, (HPSTR)LP_ret_GlobalLock_2 + *(_DWORD *)(v6 + 4) + 8, *(_DWORD *)(v4 + 4)) == *(_DWORD *)(v4 + 4) )
  {
    *(_DWORD *)(v6 + 4) += a3 + (a3 & 1);
    result = 0;
  }
  else
  {
    result = -2147205011;
  }
  return result;
}

The patch modified the function like so:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
igned int __stdcall sub_73B5A58C(int a1, int a2, int a3)
{
  DWORD v3; // ecx@1
  int v4; // edi@1
  signed int result; // eax@2
  int v6; // esi@3
  HGLOBAL hMem; // eax@5
  LPVOID LP_ret_GlobalLock_2; // ebx@7
  int v9; // eax@8
  unsigned int v10; // eax@1
  HGLOBAL v11; // eax@5
  DWORD v12; // ST04_4@5
  HGLOBAL v13; // eax@5
  LPVOID LP_ret_GlobalLock_1; // eax@7
 
  v4 = a3;
  v10 = *(_DWORD *)(a3 + 4);
  v3 = v10 + 8;
  a3 = v10 + 8;
  if ( v10 + 8 < v10 )
    return -2147205017;
  v6 = a1;
  if ( *(_DWORD *)a1 )
  {
    if ( v3 + *(_DWORD *)(a1 + 4) < v3 )
      return -2147205017;
    v11 = GlobalHandle(*(LPCVOID *)a1);
    GlobalUnlock(v11);
    v12 = a3 + *(_DWORD *)(v6 + 4);
    v13 = GlobalHandle(*(LPCVOID *)v6);
    hMem = GlobalReAlloc(v13, v12, 0x2002u);
  }
  else
  {
    hMem = GlobalAlloc(0x2002u, v3);
  }
  LP_ret_GlobalLock_1 = GlobalLock(hMem);
  LP_ret_GlobalLock_2 = LP_ret_GlobalLock_1;
  if ( !LP_ret_GlobalLock_1 || (v9 = *(_DWORD *)(v6 + 4), *(_DWORD *)v6 = LP_ret_GlobalLock_2, v9 < 0) )
    return -2147205017;
  *(_DWORD *)((char *)LP_ret_GlobalLock_2 + v9) = *(_DWORD *)v4;
  *(_DWORD *)(LP_ret_GlobalLock_2 + *(_DWORD *)(v6 + 4) + 4) = *(_DWORD *)(v4 + 4);
  sub_73B5FE37(a2, *(_DWORD *)(v4 + 12), 0);
  if ( sub_73B5FDB9(a2, (HPSTR)LP_ret_GlobalLock_2 + *(_DWORD *)(v6 + 4) + 8, *(_DWORD *)(v4 + 4)) == *(_DWORD *)(v4 + 4) )
  {
    *(_DWORD *)(v6 + 4) += a3 + (a3 & 1);
    result = 0;
  }
  else
  {
    result = -2147205011;
  }
  return result;
}

From my understanding, the patch pretty much just checks that *(_DWORD *)(v6 + 4) is more than zero before processing the rest of the code. If not it returns:

if ( !LP_ret_GlobalLock_1 || (v9 = *(_DWORD *)(v6 + 4), *(_DWORD *)v6 = LP_ret_GlobalLock_2, v9 < 0) )

I haven't located exactly where the integer overflow can be used to trigger a memory corruption bug yet so feel free to give me a hint!

Ps: Thanks for Ivanlef0u, if he ever comes on that blog post, for his patience on IRC. Share This Post

Linux kernel: uninit op in SOCKOPS_WRAP() leads to privesc

Linus Torvalds committed a silent null pointer dereference security fix on the 13th of August 2009.

736
737
738
739
740
741
742
743
        if (more)
                flags |= MSG_MORE;
 
-       return sock->ops->sendpage(sock, page, offset, size, flags);
+       return kernel_sendpage(sock, page, offset, size, flags);
 }
 
 static ssize_t sock_splice_read(struct file *file, loff_t *ppos,

The issue is very straight forward: sock->ops->sendpage is not checked to be valid before being returned.

To quote Julien Tinnes:

The issue lies in how Linux deals with unavailable operations for some protocols. sock_sendpage and others don’t check for NULL pointers before dereferencing operations in the ops structure. Instead the kernel relies on correct initialization of those proto_ops structures with stubs (such as sock_no_sendpage) instead of NULL pointers.

The fix calls the following kernel_sendpage() function which does exactly that check:

2404
2405
2406
2407
2408
2409
2410
int kernel_sendpage(struct socket *sock, struct page *page, int offset, size_t size, int flags)
{
        if (sock->ops->sendpage)
                return sock->ops->sendpage(sock, page, offset, size, flags);
 
        return sock_no_sendpage(sock, page, offset, size, flags);
}

You may want to have a look at the following urls for more stuff:

Share This Post

Bypassing PHP empty() function

Laurent Gaffié published a bug in Wordpress 2.8.3 today allowing an attacker to reset the blog administrator password without a valid $key. In other words, without confirmation.

That bug is definitely a low risk issue, if considered at all a “security bug”. However what is interesting to learn from it is what happens without the PHP code of Wordpress.

That’s the buggy function:

185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
function reset_password($key) {
	global $wpdb;
 
	$key = preg_replace('/[^a-z0-9]/i', '', $key);
 
	if ( empty( $key ) )
		return new WP_Error('invalid_key', __('Invalid key'));
 
	$user = $wpdb->get_row($wpdb->prepare("SELECT * FROM $wpdb->users WHERE user_activation_key = %s", $key));
	if ( empty( $user ) )
		return new WP_Error('invalid_key', __('Invalid key'));
 
	// Generate something random for a password...
	$new_pass = wp_generate_password();
 
	do_action('password_reset', $user, $new_pass);
 
	wp_set_password($new_pass, $user->ID);
...
}

Laurent tells us:“A web browser is sufficiant to reproduce this Proof of concept: http://DOMAIN_NAME.TLD/wp-login.php?action=rp&key[]= The password will be reset without any confirmation.”

So basically, by sending an array instead of a string, Laurent was able to bypass the empty() check at line 190. What’s even more interesting from here is the check done by Wordpress to verify if the key can be found within the database:

193
194
195
$user = $wpdb->get_row($wpdb->prepare("SELECT * FROM $wpdb->users WHERE user_activation_key = %s", $key));
if ( empty( $user ) )
	return new WP_Error('invalid_key', __('Invalid key'));

This have been placed to detect any abuse of the key. But is not functional because by default the get_row() function returns an object which is not considered empty or null by the empty() function at line 194.

The patch applied by Wordpress is the following:

190
191
- if ( empty( $key )  ) 
+ if ( empty( $key ) || !is_string( $key ) )

However this only solves partially the problem. The second check can probably still be bypassed…

Conclusion of this post: using empty() for security checks is clearly not enough; you also need to check the type of your variable and eventually it’s value.

Share This Post

Drupal – Live – Privilege escalation, Impersonation

A privilege escalation vulnerability in Drupal’s Live module was discovered reported by Roderick Muit on July 21st 2009. This is rather an exiting vulnerability because it is unlikely it would have been identified during a blackbox test.

Here’s the vulnerable code of live.node.inc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function live_node_preview() {
  global $user;
 
...
 
  $node->name = isset($_POST['username']) ? $_POST['username'] : '';
...
 
  // Load the user's name when needed.
  if (isset($node->name)) {
    // The use of isset() is mandatory in the context of user IDs, because
    // user ID 0 denotes the anonymous user.
    if ($user = user_load(array('name' => $node->name))) {
      $node->uid = $user->uid;
      $node->picture = $user->picture;
    }
    else {
      $node->uid = 0; // anonymous user
    }
  }
  else if ($node->uid) {
    $user = user_load(array('uid' => $node->uid));
    $node->name = $user->name;
    $node->picture = $user->picture;
  }
 
...
 
  exit(drupal_json(array('html' => $output, 'error' => 0)));
}

In order to understand that vulnerability, a bit of background on how Drupal code works is needed. The “global $user;” at the beginning of the function calls a Drupal object containing all the information about the current user. This object looks like that:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
    stdClass Object {
 
    // Provided by the users Table
 
    [uid] => 2 //Primary key of users table
    [name] => Joe Example
    [pass] => 9789adf798a7d8f //MD5 hash of the password
    [mail] =>joe@example.com //Current email
    [mode] => 0 //Comment-viewing preference
    [sort] => 0 //Comment-viewing preference
    [threshold] => 0 //Comment-viewing preference
    [theme] => chameleon //User's chosen theme
    [signature] => Drupal rocks! //Visible in user's comment
    [created] => 1161112061 //Unix timestamp
    [access] => 1161112061 //Unix timestamp
    [login] => 1161112317 //Unix timestamp
    [status] => 1 //0 means user is blocked
    [timezone] => -18000 //Number of seconds that is offset from GMT
    [language] => en //Set by locale_initialize() in common.inc.
    [picture] => files/pictures/me.jpg
    [init] => joe@example.com //Initial email upon registration
    [data] => //Arbitrary data stored by modules
 
    //Provided by the user_roles Table
 
    [roles] => Array ( [2] => authenticated user )
 
    //Provided by the sessions Table
 
    [sid] => 7a89sdf8glj4j345jlk43lkj5 //Session ID assigned by PHP
    [hostname] => 127.0.0.1 //IP address of user
    [timestamp] => 1161113476 //Unix timestamp of the time the user last received a completed page
    [cache] => 0 //Timestamp used for per-user caching
    [session] => user_overview_filter|a:0:{} //Arbitrary data stored by modules for the duration of the session
 
    }

So what happens in the vulnerable source code is that the module rewrite the $user object with:

if ($user = user_load(array('name' => $node->name)))

Fortunately $node->name is user controlled:

$node->name = isset($_POST['username']) ? $_POST['username'] : '';

The conclusion is that for the rest of the code being executed, every time the $user global variable will be used, the attacker would have change his original account to the one sent in $_POST['username'].

The patch:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
   if (isset($node->name)) {
     // The use of isset() is mandatory in the context of user IDs, because
     // user ID 0 denotes the anonymous user.
-    if ($user = user_load(array('name' => $node->name))) {
-      $node->uid = $user->uid;
-      $node->picture = $user->picture;
+    if ($account = user_load(array('name' => $node->name))) {
+      $node->uid = $account->uid;
+      $node->picture = $account->picture;
     }
     else {
       $node->uid = 0; // anonymous user
     }
   }
   else if ($node->uid) {
-    $user = user_load(array('uid' => $node->uid));
-    $node->name = $user->name;
-    $node->picture = $user->picture;
+    $account = user_load(array('uid' => $node->uid));
+    $node->name = $account->name;
+    $node->picture = $account->picture;
   }
 
   $node->changed = time();

A new variable $account is used instead of $user which fixes the issue.

Share This Post

kernel: ecryptfs stack overflow in parse_tag_11_packet()

A stack overflow vulnerability in ecryptfs has been reported by Ramon de Carvalho Valle. There’s the code of the vulnerable parse_tag_11_packet() function before the patch:

1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
static int
parse_tag_11_packet(unsigned char *data, unsigned char *contents,
		    size_t max_contents_bytes, size_t *tag_11_contents_size,
		    size_t *packet_size, size_t max_packet_size)
{
	size_t body_size;
	size_t length_size;
	int rc = 0;
 
	(*packet_size) = 0;
	(*tag_11_contents_size) = 0;
	/* This format is inspired by OpenPGP; see RFC 2440
	 * packet tag 11
	 *
	 * Tag 11 identifier (1 byte)
	 * Max Tag 11 packet size (max 3 bytes)
	 * Binary format specifier (1 byte)
	 * Filename length (1 byte)
	 * Filename ("_CONSOLE") (8 bytes)
	 * Modification date (4 bytes)
	 * Literal data (arbitrary)
	 *
	 * We need at least 16 bytes of data for the packet to even be
	 * valid.
	 */
	if (max_packet_size < 16) {
		printk(KERN_ERR "Maximum packet size too small\n");
		rc = -EINVAL;
		goto out;
	}
	if (data[(*packet_size)++] != ECRYPTFS_TAG_11_PACKET_TYPE) {
		printk(KERN_WARNING "Invalid tag 11 packet format\n");
		rc = -EINVAL;
		goto out;
	}
	rc = ecryptfs_parse_packet_length(&data[(*packet_size)], &body_size,
					  &length_size);
	if (rc) {
		printk(KERN_WARNING "Invalid tag 11 packet format\n");
		goto out;
	}
	if (body_size < 14) {
		printk(KERN_WARNING "Invalid body size ([%td])\n", body_size);
		rc = -EINVAL;
		goto out;
	}
	(*packet_size) += length_size;
	(*tag_11_contents_size) = (body_size - 14);
	if (unlikely((*packet_size) + body_size + 1 > max_packet_size)) {
		printk(KERN_ERR "Packet size exceeds max\n");
		rc = -EINVAL;
		goto out;
	}
	if (data[(*packet_size)++] != 0x62) {
		printk(KERN_WARNING "Unrecognizable packet\n");
		rc = -EINVAL;
		goto out;
	}
	if (data[(*packet_size)++] != 0x08) {
		printk(KERN_WARNING "Unrecognizable packet\n");
		rc = -EINVAL;
		goto out;
	}
	(*packet_size) += 12; /* Ignore filename and modification date */
	memcpy(contents, &data[(*packet_size)], (*tag_11_contents_size));
	(*packet_size) += (*tag_11_contents_size);
out:
	if (rc) {
		(*packet_size) = 0;
		(*tag_11_contents_size) = 0;
	}
	return rc;
}

memcpy does not check the size of *tag_11_contents_size: memcpy(contents, &data[(*packet_size)], (*tag_11_contents_size));

The patch that was applied:

1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
 		rc = -EINVAL;
 		goto out;
 	}
+	if (unlikely((*tag_11_contents_size) > max_contents_bytes)) {
+		printk(KERN_ERR "Literal data section in tag 11 packet exceeds "
+		       "expected size\n");
+		rc = -EINVAL;
+		goto out;
+	}
 	if (data[(*packet_size)++] != 0x62) {
 		printk(KERN_WARNING "Unrecognizable packet\n");
 		rc = -EINVAL;

Edit: RISE posted a good advisory about that vulnerability.

Share This Post

Erubis vs ERB (Ruby)

In my quest to discover if major web applications and frameworks provide effective solutions against common known attacks. I decided to check out Merb, an alternative web framework for Ruby.

Merb uses Erubis (a fast, secure, and very extensible implementation of eRuby) for it’s HTML output filtering:

203
204
205
206
# File /home/merb/merb-docs/current/gems/gems/merb-core-1.0.8.1/lib/merb-core/dispatch/request_parsers.rb, line 204
def self.escape_xml(s)
    Erubis::XmlHelper.escape_xml(s)
end

So I had a look at Erubis:

16
17
18
19
20
21
22
23
24
25
26
27
    ESCAPE_TABLE = {
      '&' => '&amp;',
      '< ' => '&lt;',
      '>' => '&gt;',
      '"' => '&quot;',
      "'" => '&#039;',
    }
 
    def escape_xml(value)
      value.to_s.gsub(/[&<>"]/) { |s| ESCAPE_TABLE[s] }   # or /[&<>"']/
      #value.to_s.gsub(/[&<>"]/) { ESCAPE_TABLE[$&] }
    end

And by default, Erubis implement the escaping: “Auto escaping (sanitizing) support, it means that ‘< %= %>‘ can be escaped in default. It is desirable for web application.“.

Good news for Ruby on Rails developers, Erubis provides support for Rails.

Share This Post

RoR – Json output escaping

Scrolling on Ruby on Rails SVN recent changesets, I came across an interesting commit: json output sanitization (changeset 9241). Which then brought my attention to the source code of RoR providing output escaping for HTML template (ERB::Util).

5
6
HTML_ESCAPE = { '&' => '&amp;',  '>' => '&gt;',   '< ' => '&lt;', '"' => '&quot;' }
JSON_ESCAPE = { '&' => '\u0026', '>' => '\u003E', '< ' => '\u003C'}

Those are the list of characters being escaped and their replacements located in erb.rb.

If we have a look at OWASP XSS Prevention cheat sheet; it is recommended to escape all those characters:

& --> &amp;
 < --> &lt;
 > --> &gt;
 " --> &quot;
 ' --> &#x27;     &apos; is not recommended
 / --> &#x2F;     forward slash is included as it helps end an HTML entity

As for JSON, the RFC is pretty straight forward:

string = quotation-mark *char quotation-mark

char = unescaped /
escape (
%x22 / ; ” quotation mark U+0022
%x5C / ; \ reverse solidus U+005C
%x2F / ; / solidus U+002F
%x62 / ; b backspace U+0008
%x66 / ; f form feed U+000C
%x6E / ; n line feed U+000A
%x72 / ; r carriage return U+000D
%x74 / ; t tab U+0009
%x75 4HEXDIG ) ; uXXXX U+XXXX

escape = %x5C ; \

quotation-mark = %x22 ; ”

unescaped = %x20-21 / %x23-5B / %x5D-10FFFF

It would be good if all of that got implemented so that Twitter’s xss pwnage history does not repeat itself…

Share This Post

Wordpress 2.8.2 Cross-site scripting

During the release of the latest wordpress 2.8.2, it was announced that a cross-site scripting vulnerability affecting the comment author url in the administration panel was fixed. For those interested in the complete source code changes, see changeset 11721.

Three notable changes have been made:

1. _wp_comment_row() in wp-admin/includes/template.php

285
286
287
288
- $author_url_display = $author_url; 
- $author_url_display = str_replace('http://www.', '', $author_url_display); 
- $author_url_display = str_replace('http://', '', $author_url_display); 
+ $author_url_display = preg_replace('|http://(www\.)?|i', '', $author_url);

2. get_comment_to_edit() in wp-admin/includes/comment.php

92
93
94
+ $comment->comment_author_url = format_to_edit( $comment->comment_author_url ); 
$comment->comment_author_url = esc_url($comment->comment_author_url); 
- $comment->comment_author_url = format_to_edit( $comment->comment_author_url );

esc_url() which escapes the urls is now being called before format_to_edit().

3. Finally, get_comment_author_url() in wp-includes/comment-template.php

196
197
198
$url = ('http://' == $comment->comment_author_url) ? '' : $comment->comment_author_url; 
+ $url = esc_url( $url, array('http', 'https') ); 
return apply_filters('get_comment_author_url', $url);

The escape function esc_url() is being applied to the url of the comment.

What’s a bit more interesting about all of that is how different websites on the internet blogged about that security issue; in other words, how they understood it.

Blogging Planet said:

A new version of the popular blogging platform Wordpress was released just a few minutes ago. It is an unexpected upgrade considering that the last Wordpress update was less than two weeks ago. The new update fixes a security vulnerability that affects all but the latest version of Wordpress.

The XSS vulnerability could be used to create comment author urls that would redirect the system administrator away from the blog’s website to another site to exploit the situation.

Well even with all this fury about cross site scripting and other browser bugs exploitation. It seems that the rest of the world still considers a cross site scripting vulnerability as something that is being used to redirect people… What about code execution in the wordpress administration panel? =)

Share This Post

Stupid magic quotes…

I’ve seen much security fail from developers but this one is priceless. I discovered the two following code snippets in CMS Made Easy:

61
62
#magic_quotes_runtime is a nuisance...  turn it off before it messes something up
set_magic_quotes_runtime(false);

and

171
172
173
174
175
176
177
178
179
#Stupid magic quotes...
if(get_magic_quotes_gpc())
{
    stripslashes_deep($_GET);
    stripslashes_deep($_POST);
    stripslashes_deep($_REQUEST);
    stripslashes_deep($_COOKIE);
    stripslashes_deep($_SESSION);
}

Also an *interesting* cross-site scripting protection the CMS implemented:

68
69
# sanitize $_GET
array_walk_recursive($_GET, 'sanitize_get_var');

the code of sanitize_get_var:

245
246
247
248
function sanitize_get_var(&$value, $key)
{
    $value = eregi_replace('\< \/?script[^\>]*\>', '', $value);
}

So much fail…

Share This Post