Кліент папрасіў нас праверыць свой інтранэт-сайт, які быў выкарыстаны супрацоўнікамі кампаніі і кліентамі. Гэта было часткай больш шырокага агляду бяспекі, і хоць мы фактычна не выкарыстоўваецца SQL-ін'екцыі, каб пранікнуць сеткі і раней, мы былі вельмі знаёмыя з агульнымі паняццямі. Мы былі цалкам паспяховымі ў гэтым баі, і хацеў расказаць мерах, прынятых у якасці ілюстрацыі.
SQL-ін'екцыя "з'яўляецца падмноствам неправераную / unsanitized уразлівасці карыстацкі ўвод (" перапаўненне буфера "з'яўляюцца розныя падмноства), і ідэя, каб пераканаць прыкладанні для запуску SQL кода, які не быў прызначаны. Калі дадатак стварае SQL радкоў наіўна "на лета", а затым запускаць іх, гэта проста стварыць некалькі сюрпрызаў.
Мы адзначаем, што гэта было некалькі звілістай дарозе з больш чым адным няправільны паварот, і іншыя з вялікім вопытам, безумоўна, маюць розныя - і лепш - падыходы. Але тое, што мы былі паспяховымі дазваляе выказаць здагадку, што мы не былі цалкам памылковымі.
Там былі і іншыя дакументы па ін'екцыі SQL, у тым ліку тыя, якія з'яўляюцца значна больш падрабязнымі, але гэта паказвае, абгрунтаванне адкрыцця столькі, колькі працэсе эксплуатацыі.
Гэта апынулася зусім карыстальніцкае дадатак, і ў нас не было папярэдніх ведаў аб прыкладання, ні доступу да зыходнага коду: гэта быў "сляпы" атакі. Трохі тыкаць паказалі, што гэты сервер IIS бег ад Microsoft 6 разам з ASP.NET, і гэта сведчыць, што база дадзеных SQL Server Microsoft: мы лічым, што гэтыя метады могуць прымяняцца да практычна любой вэб-прыкладанняў пры падтрымцы любых SQL Server.
Увайсці Старонка традыцыйных імя карыстальніка і пароля-форме, але і электроннай пошце мне мой пароль-спасылку; апошняе апынулася падзенне ўсёй сістэмы.
Пры ўводзе адрасы электроннай пошты, сістэмы меркавана паглядзеў у базу дадзеных карыстальнікаў для гэтага адрасу электроннай пошты, і што-то па пошце на гэты адрас. Так як мой электронны адрас не знойдзены, ён не збіраецца паслаць мне што-небудзь.
Такім чынам, першы тэст у любы SQL-иш форма для ўводу адной выняткай, як частка дадзеных: намер, каб убачыць, калі яны пабудаваць радок SQL літаральна без дэзінфекцыі. Пры падачы формы з цытатай ў адрас электроннай пошты, мы атрымліваем памылку 500 (збой сервера), і гэта сведчыць аб тым, што "зламаны" уваход фактычна разабраны літаральна. Бінга.
Мы лічым, што зыходны код SQL выглядае прыкладна так:
SELECT fieldlist
FROM table
WHERE field = '$EMAIL';
Тут $ электроннай пошты адрас прадстаўленых на форме карыстальніка, і больш запытаў забяспечвае двукоссі, якія ўсталёўваюць гэта як знакавая радок. Мы не ведаем канкрэтныя імёны палёў або табліц удзел, але мы ведаем іх характар, і мы зробім добры здагадкі пазней.
Калі мы ўваходзім steve@unixwiz.net "- зьвярніце ўвагу на двукоссі закрыцця - гэта дае пабудаваныя SQL:
SELECT fieldlist
FROM table
WHERE field = 'steve@unixwiz.net'';
пры гэтым выконваецца, парсер SQL знайсці дадатковыя двукоссі і перапыняе з сінтаксічная памылку. Як гэта праяўляецца для карыстальніка залежыць ад унутранай памылкі аднаўлення прыкладання працэдур, але гэта, як правіла, адрозніваецца ад "адрас электроннай пошты, невядома". Гэтая памылка адказ мёртвых паддаўкі, што ўводзяцца карыстальнікам дадзеныя не прадэзінфікаваць правільна і што прыкладанне гатова для эксплуатацыі.
Так як дадзеныя мы запаўненне ўяўляецца, у ИНЕКЕ, давайце зменім характар, што агаворка ў SQL законным шляхам і паглядзець, што адбываецца. Пры ўводзе 'нічога' ці х '=' х, у выніку SQL з'яўляецца:
SELECT fieldlist
FROM table
WHERE field = 'anything' OR 'x'='x';
Так як дадатак на самай справе не думаў пра запыту - проста пабудовы радкі - наша выкарыстанне цытат ператварыўся аднакампанентная ИНЕКЕ ў двухкампанентны адзін, і 'х' = х 'прапанову' гарантавана, каб быць праўдай незалежна ад таго, што першы пункт з'яўляецца (ёсць лепшы падыход для гэтага "заўсёды так" частка, мы пагаворым пазней).
Але ў адрозненне ад "рэальнай" запыт, які павінен вяртаць толькі адзін элемент кожны раз, гэтая версія будзе істотна вярнецеся кожны элемент у базе даных членаў. Адзіны спосаб даведацца, якія прыкладання будуць рабіць у гэтую акалічнасць, каб паспрабаваць. Такім чынам, мы былі сустрэтыя з:
Ваш Лагін інфармацыя была почте@example.com random.person.
Нашы лепшыя думаю, што гэта першая запіс, вяртаюцца запытам, эфектыўна ўступлення узятых наўздагад. Гэты чалавек сапраўды атрымаць гэта забыліся пароль-спасылку па электроннай пошце, якая, верагодна, стала сюрпрызам для яго і можа паставіць папераджальныя сцягі где-то.
Мы цяпер ведаем, што мы можам маніпуляваць на запыт нашых мэтаў, хоць мы яшчэ не ведаем многае пра яго часткі мы не бачым. Але мы назіралі тры розных адказаў на нашы розныя ўваходы:
Першыя дзве ўяўляюць сабой рэакцыю на правільна сфармаваны SQL, у той час як апошняя з'яўляецца для дрэнных SQL: гэта адрозненне будзе вельмі карысна, калі спрабуе адгадаць структуру запыту.
Першыя крокі да мяркую, што некаторыя імёны палёў: мы дастаткова ўпэўненыя, што запыт утрымлівае "электронны адрас" і "Пароль", і там могуць быць рэчы, як "ЗША ліст" або "ідэнтыфікатар карыстальніка" ці "нумар тэлефона". Мы б вельмі люблю выконваць SHOW TABLE, але ў дадатак да не ведаючы імя табліцы, не існуе відавочных транспартнага сродку, каб атрымаць выснову гэтай каманды накіроўваюцца да нас.
Такім чынам, мы зробім гэта па крокаў. У кожным выпадку, мы будзем паказваць увесь запыт як мы яго ведаем, з нашай уласнай фрагменты паказалі адмыслова. Мы ведаем, што хвост запыт параўнанні з адрасу электроннай пошты, так што давайце думаю электроннай пошты ў якасці імя поля:
SELECT fieldlist
FROM table
WHERE field = 'x' AND email IS NULL; --';
Мэта заключаецца ў выкарыстанні прапанаванага наймення поля (электроннай пошце) у пабудаваным запыт і высветліць, калі SQL з'яўляецца сапраўдным ці не. Мы не клапоцімся аб адпаведнасці адрас электроннай пошты (менавіта таму мы выкарыстоўваем манекена "х"), і - адзначае пачатак каментара SQL. Гэта эфектыўны спосаб "спажываць" канчатковага цытатай забяспечваецца шляхам прымянення і не турбавацца аб адпаведнасці іх.
Калі мы атрымаем памылка сервера, гэта азначае, што нашы SQL будзе няслушным і сінтаксічныя памылкі быў кінуты: ён, хутчэй за ўсё, з-за дрэннай імя поля. Калі мы атрымаем любы правільны адказ, мы здагадаліся, імя правільна. Гэта тычыцца ці мы атрымаць "электронны невядома" ці "Пароль быў адпраўлены" адказ.
Аднак варта адзначыць, што мы выкарыстоўваем разам і замест OR: гэта наўмысна. У адлюстраванне фазы SQL схему, мы сапраўды не звязаных з адгадаць якой-небудзь канкрэтнай адрасы электроннай пошты, і мы не хочам выпадковых карыстальнікаў заваленыя "Вось ваш пароль" лісты з дадатку - гэта, безумоўна, выклікаць падазрэнні ні да чаго добрага мэты. Пры выкарыстанні сумесна з І адрас электроннай пошты, які не мог быць сапраўдным, мы ўпэўненыя, што запыт будзе заўсёды вяртаць нуль радкоў і ніколі не генерыраваць пароль-напамін па электроннай пошце.
Адпраўка вышэй фрагмент сапраўды даў нам "адрас электроннай пошты невядомыя" адказ, так што зараз мы ведаем, што адрас электроннай пошты захоўваецца ў полі электроннай пошце. Калі б гэтага не працавалі, мы б пастараліся email_address або па пошце ці як. Гэты працэс будзе ўключаць даволі шмат гадаць.
Далей мы мяркую, што некаторыя іншыя відавочныя назвы: пароль, ідэнтыфікатар карыстальніка, імя і таму падобнае. Гэта ўсё робіцца па аднаму, і нічога, акрамя "адмовы сервера" азначае, што мы адгадалі назву правільна.
SELECT fieldlist
FROM table
WHERE email = 'x' AND userid IS NULL; --';
У выніку гэтага працэсу, мы знайшлі некалькі дапушчальных імёнаў поля:
Ёсць, безумоўна, больш (і добрая крыніца ключы, гэта імёны палёў на формах), але трохі капаць не выявілі. Але мы яшчэ не ведаем, імя табліцы, што гэтыя палі знаходзяцца у - як пазнаць?
Ужыванне убудаваных у запыт ўжо ёсць імя табліцы убудаваныя ў яе, але мы не ведаем, што гэта імя: Ёсць некалькі падыходаў, што для знаходжання (і іншыя) імёны табліц. Той, які мы прынялі быў спадзявацца на подзапросов.
Аўтаномны запыт
SELECT COUNT(*) FROM tabname
Вяртае колькасць запісаў у гэтай табліцы, і, вядома, не выконваецца, калі імя табліцы, невядома. Мы можам пабудаваць гэта ў нашым радок зонд для імя табліцы:
SELECT email, passwd, login_id, full_name
FROM table
WHERE email = 'x' AND 1=(SELECT COUNT(*) FROM tabname); --';
Нам усё роўна, колькі запісаў ёсць, вядома, ці толькі імя табліцы з'яўляецца сапраўдным ці не. Паўтараючы за некалькі здагадак, мы ў канчатковым выніку вызначаецца, што члены былі сапраўдныя табліцы ў базе дадзеных. Але гэта табліца, выкарыстоўваная ў гэтым запыце? Для гэтага нам спатрэбіцца яшчэ адно выпрабаванне з выкарыстаннем табліцы. Поля абазначэння ён працуе толькі для табліц, якія з'яўляюцца фактычна часткай гэтага запыту, а не толькі тое, што табліца існуе.
SELECT email, passwd, login_id, full_name
FROM members
WHERE email = 'x' AND members.email IS NULL; --';
Пры гэтым вяртаецца "Email невядома", ён пацвердзіў, што наша SQL быў добра сфарміраваны і што мы правільна здагадаліся, імя табліцы. Гэта будзе важна пазней, але замест гэтага мы ўзялі іншы падыход у прамежкавы перыяд.
На дадзены момант мы маем частковае ўяўленне аб структуры табліцы членаў, але мы ведаем толькі аднаго імя карыстальніка: выпадковых членаў, якія атрымалі нашы першапачатковыя "Вось ваш пароль" па электроннай пошце. Нагадаем, што мы ніколі не атрымалі паведамленне, а толькі адрас было адпраўлена ліст. Мы хацелі б атрымаць яшчэ некалькі імёнаў для працы, пажадана тых, верагодна, маюць доступ да больш дадзеных.
Першае месца, каб пачаць, вядома, вэб-сайт кампаніі, каб высветліць, хто ёсць хто: "Аб нас" ці "Кантакт" Старонкі з спісу, які часта працуе месцы. Многія з іх утрымоўваюць адрасы электроннай пошты, але нават тыя, якія не пералічыць іх можа даць нам некаторыя падказкі, якія дазваляюць нам знайсці іх з дапамогай нашага інструмента.
Ідэя прадставіць запыт, які выкарыстоўвае падабаецца прапанова, што дазваляе нам рабіць частковыя супадзення імёнаў ці адрасоў электроннай пошты ў базу дадзеных, кожны раз выклікаючы "Мы накіравалі свой ??пароль" паведамленняў і электроннай пошце:. Папярэджанне хоць гэта паказвае, адрас электроннай пошты Кожны раз, калі мы запускаем яго, ён таксама фактычна адпраўляе гэта паведамленне, якое можа выклікаць падазрэнні. Гэта дазваляе выказаць здагадку, што мы прымаем гэта лёгка.
Мы можам зрабіць запыт па электроннай пошце імя або прозвішча, імя (або як мяркуецца, іншую інфармацыю), кожны раз, пакласці ў% маскі, што, як падтрымлівае:
SELECT email, passwd, login_id, full_name
FROM members
WHERE email = 'x' OR full_name LIKE '%Bob%';
Майце на ўвазе, што, хоць можа быць і больш, чым адзін "Боб", мы толькі атрымліваем, каб убачыць адзін з іх: гэта прадугледжвае перапрацоўку нашых падабаецца прапанова вузка.
У канчатковым рахунку, мы можа спатрэбіцца толькі адзін сапраўдны адрас электроннай пошты, каб выкарыстаць нашы шляхі цалі
Вядома, можна спрабаваць грубай сілай угадвання пароля на галоўнай старонцы Лагін, але шматлікія сістэмы робяць намаганні па выяўленні або нават прадухіліць гэта. Там можа быць логах, блакіроўкі уліковых запісаў, або іншыя прылады, якія б істотна перашкаджаць нашым намаганням, але з-за не-санітарнай апрацоўцы ўваходу, у нас ёсць іншы шлях, які значна менш верагодна, так абаронены.
Мы замест гэтага фактычнага тэсціравання пароль у нашай фрагмент па электроннай пошце ў тым ліку імя і пароль непасрэдна. У нашым прыкладзе мы будзем выкарыстоўваць нашы ахвяры, bob@example.com і паспрабуйце некалькі пароляў.
SELECT email, passwd, login_id, full_name
FROM members
WHERE email = 'bob@example.com' AND passwd = 'hello123';
Гэта добра добра сфармаваныя SQL, таму мы не чакаем убачыць любыя памылкі сервера, і мы будзем ведаць, мы выявілі, пароль, калі мы атрымаем "Ваш пароль быў высланы Вам" паведамленні. Нашы маркі ў цяперашні час шапнулі, але ў нас ёсць свой пароль.
Гэтая працэдура можа быць аўтаматызаваны з дапамогай сцэнарыяў на Perl, і хоць мы знаходзіліся ў працэсе стварэння гэтага сцэнарыя, мы ў канчатковым выніку адбываецца па іншай дарозе, перш чым на самай справе спрабуе яго.
Да гэтага часу мы нічога не зрабілі, але запыт да базы дадзеных, і, хоць SELECT толькi для чытання, гэта не азначае, што SQL з'яўляецца. SQL выкарыстоўвае кропку з коскай для спынення заяву, і, калі ўвод не прадэзінфікаваць належным чынам, не можа быць нічога, што перашкаджае нам струн нашай каманды звязаны ў канцы запыту.
Найбольш рэзкае прыклад:
SELECT email, passwd, login_id, full_name
FROM members
WHERE email = 'x'; DROP TABLE members; --'; -- Boom!
Першая частка забяспечвае фіктыўны адрас электроннай пошты - "х" - і мы не хвалюе, што гэты запыт вяртае: мы толькі што з дарогі, каб мы маглі ўвесці звязаных каманду SQL. Гэта адзін спрабуе выдаліць (выдаліць) усю табліцу членаў, якія сапраўды не здаецца занадта спартовым.
Гэта паказвае, што не толькі мы можам запускаць асобныя каманды SQL, але мы можам таксама змяніць базу дадзеных. Гэта абнадзейвае.
Улічваючы, што мы ведаем частковае структура табліцы членаў, здаецца праўдападобным падыход да спробе дадаць новую запіс у гэтай табліцы: Калі гэта працуе, мы проста быць у стане Увайсці непасрэдна з нашымі зноў ўстаўляецца паўнамоцтвы.
Гэта не дзіўна, што займае крыху больш SQL, і мы загарнулі яго на працягу некалькіх ліній для прастаты выкладу, але са свайго боку яшчэ адзін бесперапынны радкі:
SELECT email, passwd, login_id, full_name FROM members WHERE email = 'x'; INSERT INTO members ('email','passwd','login_id','full_name') VALUES ('steve@unixwiz.net','hello','steve','Steve Friedl');--';
Нават калі мы фактычна атрымалі нашы палі і імёны табліц права, некалькі рэчаў, мог трапіць у наш спосаб паспяховай атакі:
У нашым выпадку, мы трапілі на кантрольна-прапускным пункце або № 4 або № 5 - мы не можам быць упэўненыя, - таму што, калі збіраецца асноўны Лагін старонку і увёўшы ў вышэй імя карыстальніка + пароль, памылка сервера быў вернуты. Гэта кажа аб тым, што палі, мы не запоўніць маюць жыццёва важнае значэнне, але ўсё ж не правільна.
Адзін з магчымых падыходаў тут спрабуе адгадаць іншых галінах, але гэта абяцае быць доўгім і працаёмкім працэсам: хоць мы можам здагадацца, іншыя "відавочна" палёў, гэта вельмі цяжка ўявіць сабе большы малюнку арганізацыі гэтага дадатку.
Мы скончылі спускаючыся па іншаму шляху.
Мы потым зразумелі, што хоць мы не ў стане дадаць новую запіс у базу дадзеных членаў, мы можам змяніць існуючы, і гэта апынулася падыход, які атрымаў нас запіс.
З папярэдняга кроку, мы ведалі, што bob@example.com быў рахунак у сістэме, і мы выкарыстоўвалі SQL-ін'екцыі, каб абнавіць свае запісы базы дадзеных з наш адрас электроннай пошты:
SELECT email, passwd, login_id, full_name FROM members WHERE email = 'x'; UPDATE members SET email = 'steve@unixwiz.net' WHERE email = 'bob@example.com';
Пасля выканання гэтага, мы, вядома, атрымаў "мы не ведалі, Ваш адрас электроннай пошты", але гэта было чакаць у сувязі з фіктыўны адрас электроннай пошты пры ўмове. Абнаўленне не будзе зарэгістраваны ў дадатку, таму яно выконваецца ціха.
Затым мы выкарыстоўвалі звычайныя "Я страціў свой пароль" спасылку - з абноўленым адрас электроннай пошты - і праз хвіліну атрымаў гэты ліст:
From: system@example.com To: steve@unixwiz.net Subject: Intranet login This email is in response to your request for your Intranet log in information. Your User ID is: bob Your password is: hello
Зараз ён быў зараз усяго толькі пытанне наступны стандарт Увайсці працэсу для доступу да сістэмы, як высокапастаўлены супрацоўнік МДП, і гэта было значна вышэй, магчыма, карыстальнік з абмежаванымі правамі, што мы маглі б стварылі з нашым падыходам INSERT.
Мы выявілі, інтранэт-сайт, каб быць дастаткова усёабдымнай, і ён уключаны - сярод іншага - спіс усіх карыстальнікаў. Гэта справядлівае заклад, што многія сайты Інтранэт таксама мець рахункі ў карпаратыўнай сеткі Windows, і, магчыма, некаторыя з іх выкарыстоўвалі той жа пароль ў абодвух месцах. Паколькі ясна, што ў нас ёсць просты спосаб для атрымання любой пароль Інтранэт, і так як мы былі размешчаны адкрытыя PPTP VPN порт на карпаратыўны брандмауэр, яно павінна быць проста спроба такога доступу.
Мы зрабілі выбарачнай праверкі на некалькі рахункаў без поспеху, і мы не можам ведаць, што гэта "няправільны пароль" ці "імя Інтранэт рахунку адрозніваецца ад імя ўліковага запісу Windows". Але мы лічым, што аўтаматызаваныя сродкі маглі б зрабіць некаторыя гэтага лягчэй.
У дадзеным канкрэтным удзел, мы атрымалі досыць доступу, што мы не зразумелі, трэба зрабіць значна больш, але і іншыя крокі маглі б быць прынятыя. Мы пагаворым аб тым, што мы можам думаць цяпер, хоць мы цалкам упэўнены, што гэта не з'яўляецца вычарпальным.
Мы таксама разумеем, што не ўсе падыходы працаваць з усімі базамі дадзеных, і мы можам спыніцца на некаторых з іх.
Мы лічым, што распрацоўнікі вэб-прыкладанняў, часта проста не думаюць пра "сюрпрыз ўваходу", але бяспеку людзей (у тым ліку дрэнных хлопцаў), так Ёсць тры асноўныя падыходы, якія могуць быць ужытыя тут.
abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789 @.-_+
Бакавая панэль на адрасы электроннай поштыВажна адзначыць, што адрасы электроннай пошты, у прыватнасці, клапотна для праверкі праграмна, таму што ўсё, падобна, яго ўласнае ўяўленне аб тым, што прымушае "сапраўдны", і гэта ганьба, каб выключыць добры адрас электроннай пошты, паколькі яна ўтрымлівае сымбаль Вам не ' T думаць.
Адзіная рэальная ўлада RFC 2822 (які ўключае ў сябе больш знаёмым RFC822), і ўключае ў сябе дастаткова шырокага вызначэння таго, што дазволена. Сапраўды педантычны можа таксама пажадаць прыняць адрасы электроннай пошты з амперсанда і зорачкі (паміж іншым), як у сіле, але іншыя - у тым ліку гэтага аўтара - задаволеныя разумнае падмноства, што ўключае ў сябе "самы" адрасоў электроннай пошты.
Тыя, якія бяруць больш абмежавальны падыход павінен быць цалкам дасведчаныя аб наступствах выключэння гэтых адрасоў, асабліва ўлічваючы, што лепшыя метады (падрыхтоўка і выкананне, захоўваемых працэдур) ліквідаваць праблемы бяспекі якіх гэтыя "няцотны" знакаў цяперашні час.
SELECT fieldlist
FROM table
WHERE id = 23 OR 1=1; -- Boom! Always matches!
SELECT fieldlist
FROM customers
WHERE name = 'Bill O''Reilly'; -- works OK
SELECT fieldlist
FROM customers
WHERE name = '\''; DROP TABLE users; --'; -- Boom!
$sth = $dbh->prepare("SELECT email, userid FROM members WHERE email = ?;");
$sth->execute($email);
Statement s = connection.createStatement();
ResultSet rs = s.executeQuery("SELECT email FROM member WHERE name = "
+ formField); // *boom*
PreparedStatement ps = connection.prepareStatement( "SELECT email FROM member WHERE name = ?"); ps.setString(1, formField); ResultSet rs = ps.executeQuery();
Звярніце ўвагу, што не ўсе базы дадзеных настроены такім жа чынам, і не ўсе нават падтрымку жа дыялекце SQL ("S" азначае "структураванай", не "Стандарт"). Напрыклад, большасць версій MySQL не падтрымлівае подзапросов, і яны не дазваляюць звычайна некалькі аператараў: яны значна ўскладняюць фактараў пры спробе пранікнуць у сетку.
Мы хацелі б падкрэсліць, што хоць мы абралі "Забыліся пароль", каб атака ў дадзеным выпадку, гэта было не зусім, таму што гэта асаблівасць вэб-прыкладанняў з'яўляецца небяспечным. Ён быў проста адным з некалькіх даступных функцый, якія маглі быць уразлівым, і было б памылкай канцэнтравацца на "Забыліся пароль" аспект прэзентацыі.
Гэта Тэхналогія Савет не быў прызначаны для забеспячэння поўнага ахопу на ін'екцыі SQL, ці нават падручнік: яна проста дакументы працэсу, развіваліся на працягу некалькіх гадзін у час удзелу кантракце. Мы бачылі іншых работ па ін'екцыі SQL абмеркаваць тэхнічную падрыхтоўку, але ўсё ж толькі забяспечыць "грошы стрэл", што ў канчатковым рахунку атрымала ім доступ.
Але, што канчатковы справаздачу неабходных базавых ведаў, каб зняць, і працэс збору інфармацыі, што мае свае плюсы таксама. Не заўсёды ёсць доступ да зыходнага коду дадатку, і магчымасць для нападу карыстальніцкае дадатак слепа мае некаторы значэнне.
Дзякуючы Дэвід Личфилд і Рэндал Шварц за тэхнічны ўклад у гэтай паперы, і на вялікі Крыс Mospaw для графічнага дызайну (© 2005 Крыс Mospaw, з дазволу аўтара).
Зменены: пар 10 Кастрычнік 6:28:06 PDT 2007
Popular Links